├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .lintstagedrc ├── .markdownlintrc ├── .npmignore ├── LICENSE ├── README.md ├── composer.json ├── package.json ├── phpcs.xml ├── rollup.config.js ├── src-js └── index.js └── src-php ├── Config └── novapages.php ├── Database ├── factories │ ├── .gitignore │ └── PageFactory.php ├── migrations │ ├── .gitignore │ ├── 2018_10_09_000000_create_pages_table.php │ ├── 2019_11_06_000000_update_pages_table_add_language.php │ ├── 2019_11_18_000000_update_pages_table_add_video.php │ └── 2020_01_01_000000_update_pages_table_add_alternative_text.php └── seeds │ ├── .gitignore │ └── PageSeeder.php ├── Events └── NovaPagesProviderRegistered.php ├── Http ├── Controllers │ └── PageController.php └── Middleware │ ├── RedirectHomepageSlugToRoot.php │ └── ServePages.php ├── Models └── Page.php ├── Nova ├── Filters │ ├── ActiveState.php │ └── PageType.php ├── NovaPagesTool.php ├── Page.php └── PageRepeaters.php ├── NovaPages.php ├── Policies └── PagePolicy.php ├── Providers ├── AuthServiceProvider.php ├── PackageServiceProvider.php └── RouteServiceProvider.php ├── Resources ├── assets │ └── js │ │ └── .gitignore ├── lang │ └── en.json └── views │ ├── show.blade.php │ └── templates │ └── default.blade.php ├── Routes ├── pages.php └── web.php └── Tests └── PageTest.php /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015-rollup" 4 | ], 5 | "plugins": [ 6 | "transform-object-rest-spread" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dewsign/nova-pages/1a00ef922dc78c8b5b682d9c532b55a64f5e7e23/.eslintignore -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "semi": [ 4 | "error", 5 | "never" 6 | ], 7 | "indent": [ 8 | "error", 9 | 4 10 | ], 11 | "no-console": 1, 12 | "radix": 0, 13 | "vue/html-indent": ["error", 4] 14 | }, 15 | "parserOptions": { 16 | "parser": "babel-eslint", 17 | "ecmaVersion": 6, 18 | "sourceType": "module", 19 | "impliedStrict": true 20 | }, 21 | "env": { 22 | "es6": true, 23 | "browser": true, 24 | "node": true 25 | }, 26 | "extends": [ 27 | "plugin:vue/recommended", 28 | "airbnb-base" 29 | ], 30 | "plugins": [ 31 | "vue" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /node_modules/ 3 | /dist-js/ 4 | .vs 5 | composer.lock 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.php": [ 3 | "./vendor/bin/phpcs" 4 | ], 5 | "*.js": [ 6 | "eslint" 7 | ], 8 | "*.vue": [ 9 | "eslint" 10 | ], 11 | "*.md": [ 12 | "markdownlint" 13 | ], 14 | "*.json": [ 15 | "jsonlint" 16 | ], 17 | ".*rc": [ 18 | "jsonlint" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.markdownlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "line-length": false 4 | } 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | !dist 2 | !readme.md 3 | !license 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 thesold 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pages module for Laravel Nova 2 | 3 | Easily create custom pages in Nova using repeaters blocks for content. See documentation for [Repeater Blocks](https://github.com/dewsign/nova-repeater-blocks) for details. 4 | 5 | Pages can be nested (as many levels deep as required), e.g. `/page-one/page-two/page-three/page-four` and a page can have any number of child pages. 6 | 7 | Even though this package does not provide any kind of layout or design, the Page model makes use of various traits and base model of the Maxfactor Laravel Support package to provide a lot of common functionality typically expected with public facing web pages, including Search Engine Optimisation (SEO). See the [Maxfactor Laravel Support](https://github.com/dewsign/maxfactor-laravel-support) repo for more information. 8 | 9 | ## Installation 10 | 11 | `composer require dewsign/nova-pages` 12 | 13 | Run the migrations 14 | 15 | ```sh 16 | php artisan migrate 17 | ``` 18 | 19 | Load the tool in your NovaServiceProvider.php 20 | 21 | ```php 22 | public function tools() 23 | { 24 | return [ 25 | ... 26 | new \Dewsign\NovaPages\Nova\NovaPagesTool, 27 | ... 28 | ]; 29 | } 30 | ``` 31 | 32 | ## Default Fields 33 | 34 | | Title | Description | 35 | | ----- | ----------- | 36 | | Active | A boolean toggle to determine if a page should be treated as active. Inactive pages can still be accessed by default. A scope is provided to help filter these. E.g. `Page::active()->get()`. | 37 | | Featured | A boolean toggle to determine if a page should be treated as featured. Use the provided scope to access them if suitable to do so in a specific situation. `Page::featured()->get()` | 38 | | Priority | A numeric field which can be used as a way of sorting and prioritising pages. Use the scopes `Page::highToLow()->get()` or `Page::lowToHigh()->get` to sort by priority. | 39 | | Name | The name of the page, accessible via the `name` attribute | 40 | | Slug | Auto-completed from the name but can be customised. This defines the url used to access the page. E.g. `mydomain.com/slug` | 41 | | Parent | Lookup field to assign the current page to a parent page. | 42 | | Image | A default image for this page. Not output anywhere by default. Access via the `image` attribute. | 43 | | Summary | A free text field to provide a summary | 44 | | H1 | An option to define unique text for the page's H1. Default to the name field on first save. | 45 | | Browser Title | An option to define unique text for the page's browser title. Default to the name field on first save. | 46 | | Nav Title | An option to define unique text for the page's navigation title. Default to the name field on first save. | 47 | | Meta Description | An option to define unique text for the page's meta description. | 48 | 49 | Using the provided scopes makes for a nice fluent API to lookup pages. 50 | E.g. `Page::active()->featured()->highToLow()->take(3)->get()`. 51 | 52 | ## Templates 53 | 54 | The packages doesn't come with any pre-made templates. Simply replace the published `resources/views/vendor/nova-pages/show.blade.php` view or create new templates inside the `resources/views/vendor/nova-pages/templates` folder. When more than one template exists, a select option will be displayed within nova where you can select the template for the page. 55 | 56 | ## Configuration 57 | 58 | ### Repeaters 59 | 60 | Nova Pages will use the Repeater Blocks defined in the [Nova Repeater Blocks](https://github.com/dewsign/nova-repeater-blocks) package config by default and you can add additional repeater blocks by adding them to the nova-pages config file. 61 | 62 | ```php 63 | 'repeaters' => [ 64 | 'More\Repeaters' 65 | ], 66 | ``` 67 | 68 | Alternatively you can remove all standard repeaters and use your own selection. 69 | 70 | ```php 71 | 'replaceRepeaters' => true, 72 | ``` 73 | 74 | ### Homepage / Default page 75 | 76 | You can define which page slug should be loaded as the homepage, accessible at `/`, unless you already have a custom route defined, in which case Nova Pages will **not** assign the homepage route! 77 | 78 | In the below example, creating a Page with the `homepage` slug will be served as the default page when accessing the website. 79 | 80 | ```php 81 | 'homepageSlug' => 'homepage', 82 | ``` 83 | 84 | ### Customisation 85 | 86 | If you want more control, you can specify which Nova Resource and Page model to use. Because of the way nova reads the model from a static variable you **must** provide your own custom resource if you want to use a custom model. 87 | 88 | In all cases, these should extend the default Page models and resource in this package. 89 | 90 | ```php 91 | 'models' => [ 92 | 'page' => 'App\Page', 93 | ], 94 | 'resources' => [ 95 | 'page' => 'App\Nova\Page', 96 | ], 97 | ``` 98 | 99 | ### Nova Resource Group 100 | 101 | ```php 102 | 'group' => 'Pages', 103 | ``` 104 | 105 | You can customise where you want the Pages resource to appear inside Laravel Nova. By default it will sit all by itself in a Pages section. 106 | 107 | ## Routing 108 | 109 | This package makes use of a catch-all route, so any url not caught by the applications web routes will be captured and processed to find a page matching the slug. This allows us to make all pages are accessible without any specific prefix. 110 | 111 | A 404 error will be thrown if no matching page was found. 112 | 113 | ## Factories & Seeders 114 | 115 | The package comes with pre-made factories and seeders. Should you wish to include them in your application simply call the seeder or use the factory provided to populate your database with some sample content. 116 | 117 | ```php 118 | // database/seeds/DatabaseSeeder.php 119 | 120 | public function run() 121 | { 122 | $this->call(Dewsign\NovaPages\Database\Seeds\PageSeeder::class); 123 | } 124 | ``` 125 | 126 | ## Domain Maps 127 | 128 | You can map sub-domains (e.g. careers.mydomain.com) to Pages to automatically serve pages matching the base slug, and any child pages, on this domain. Any number of sub-domains are supported by adding the slugs to the `domainMap` array in the configuration file. 129 | 130 | **Important**: If you are using domain maps, you must set the `rootDomain` in your config or provided env lookup for `ROOT_DOMAIN`. E.g. `mydomain.com`. 131 | 132 | ```php 133 | return [ 134 | ... 135 | 'domainMap' => [ 136 | 'careers', 137 | ], 138 | ]; 139 | ``` 140 | 141 | Int he above example, if you create a Page with the `careers` slug, this and any sub-pages will be served on the sub-domain with the base slug removed. E.g. `mydomain.com/careers/vacancies` will become `careers.mydomain.com/vacancies`. 142 | 143 | *Important: You will need to ensure that any non-page routes go to the the correct domain name. We recommend always routing to full URLs rather than relative path.* 144 | 145 | We provide two route helpers, you can safely use the `pages.show` route name if you are not using domain maps at all. If you are using domain maps, you can use the `domain.pages.show` helper which accepts an additional domain parameter. 146 | 147 | ```php 148 | route('pages.show', ['path' => 'careers']); 149 | // https://mydomain.com/careers 150 | 151 | route('domain.pages.show', ['domain' => 'careers', 'path' => 'vacancies']); 152 | // https://careers.mydomain.com/vacancies 153 | ``` 154 | 155 | ## Permissions 156 | 157 | A PagePolicy is included, but not loaded by default, for the [Nova Permissions Tool](https://github.com/Silvanite/novatoolpermissions), which uses [Brandenburg](https://github.com/Silvanite/brandenburg) under the hood. Simply load the AuthServiceProvider from this package to make use of this or ignore this if you are using an alternative permissions library. 158 | 159 | ```php 160 | // config/app.php 161 | 162 | 'providers' => [ 163 | ... 164 | Dewsign\NovaPages\Providers\AuthServiceProvider::class, 165 | ], 166 | ``` 167 | 168 | ## Helpers 169 | 170 | ### Meta pages 171 | 172 | When working with sections of your application which are routed to their own controllers you may still want to allow users to add additional content and manage meta information through the UI. You can include the provided `meta` helper to retrieve this meta data from Pages. 173 | 174 | ```php 175 | $page = Page::meta('page-slug', 'Default Text'); 176 | ``` 177 | 178 | Will return the Page model if a matching page is found, otherwise will return an array with defaults ... 179 | 180 | ```php 181 | [ 182 | 'page_title' => 'Default Text', 183 | 'browser_title' => 'Default Text', 184 | 'meta_description' => 'Default Text', 185 | 'h1' => 'Default Text', 186 | ]; 187 | ``` 188 | 189 | Use the meta page to provide this content to the front-end means you can re-use a lot of code across all sections of your site to keep things consistent and benefit from the included SEO value. 190 | 191 | ### Language 192 | 193 | You are able to change what html lang type the page will be on creation. You can enable this functionality by setting the `enableLanguageSelection` config value to `true`; 194 | 195 | To implement this into your markup, we suggest adding this to your `layouts.default` view: 196 | 197 | ```php 198 | 199 | ``` 200 | 201 | There is a `defaultLanguage` config variable that you are free to customise to suit your application. This package comes with `en-GB` set as the default language. 202 | 203 | 204 | ## Access Control 205 | 206 | This package makes use of [silvanite/novatoolpermissions](https://github.com/Silvanite/novatoolpermissions) to handle access control to pages. Please refer to the documentation for `novatoolpermissions` for more info. 207 | 208 | The function `authoriseToView` on the `Page` model can be overridden to customise the access control functionality. Alternatively, use the `accessContent` gate in the PagePolicy. 209 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dewsign/nova-pages", 3 | "description": "Pages module for Laravel Nova", 4 | "type": "package", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Marco Mark", 9 | "email": "m2de@outlook.com" 10 | } 11 | ], 12 | "minimum-stability": "dev", 13 | "prefer-stable": true, 14 | "require": { 15 | "php": ">=7.0.0", 16 | "laravel/framework": ">=5.0.0", 17 | "maxfactor/support": "^2.0.0", 18 | "benjaminhirsch/nova-slug-field": "^1.0", 19 | "dewsign/nova-repeater-blocks": "^1.2.0", 20 | "epartment/nova-dependency-container": "^1.1", 21 | "silvanite/brandenburg": "^1.0", 22 | "silvanite/novatoolpermissions": "^1.1", 23 | "rinvex/countries": "^6.1" 24 | }, 25 | "require-dev": { 26 | "squizlabs/php_codesniffer": ">=3.1" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "Dewsign\\NovaPages\\": "src-php/" 31 | } 32 | }, 33 | "extra":{ 34 | "laravel": { 35 | "providers": [ 36 | "Dewsign\\NovaPages\\Providers\\PackageServiceProvider", 37 | "Dewsign\\NovaPages\\Providers\\AuthServiceProvider" 38 | ] 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dewsign/nova-pages", 3 | "version": "1.5.6", 4 | "description": "Pages module for Laravel Nova", 5 | "main": "dist-js/index.js", 6 | "scripts": { 7 | "build": "rollup -c", 8 | "watch": "rollup -mwc", 9 | "precommit": "lint-staged", 10 | "prepublish": "npm run build", 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/dewsign/nova-pages.git" 16 | }, 17 | "keywords": [ 18 | "boilerplate" 19 | ], 20 | "author": "Marco Mark ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/dewsign/nova-pages/issues" 24 | }, 25 | "homepage": "https://github.com/dewsign/nova-pages#readme", 26 | "devDependencies": { 27 | "babel-core": "^6.26.3", 28 | "babel-eslint": "^8.2.6", 29 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 30 | "babel-preset-es2015-rollup": "^3.0.0", 31 | "eslint": "^5.1.0", 32 | "eslint-config-airbnb-base": "^13.0.0", 33 | "eslint-plugin-import": "^2.13.0", 34 | "eslint-plugin-vue": "^4.7.0", 35 | "husky": "^0.14.3", 36 | "jsonlint": "^1.6.3", 37 | "lint-staged": "^7.2.0", 38 | "markdownlint-cli": "^0.11.0", 39 | "rollup": "^0.63.2", 40 | "rollup-plugin-babel": "^3.0.7", 41 | "rollup-plugin-vue": "^4.3.1", 42 | "vue-template-compiler": "^2.5.16" 43 | }, 44 | "dependencies": {} 45 | } 46 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | The PHP Coding Standards 12 | 13 | 20 | */*.php 21 | 22 | 27 | */autoload.php 28 | */docs/* 29 | */vendor/* 30 | */node_modules/* 31 | */database/* 32 | 33 | 40 | 41 | 42 | 43 | 44 | 48 | 49 | 50 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import RollupPluginVue from 'rollup-plugin-vue' 2 | import RollupPluginBabel from 'rollup-plugin-babel' 3 | 4 | export default { 5 | input: 'src-js/index.js', 6 | output: { 7 | file: 'dist-js/index.js', 8 | format: 'cjs', 9 | }, 10 | external: [ 11 | 'vue', 12 | ], 13 | plugins: [ 14 | RollupPluginVue(), 15 | RollupPluginBabel(), 16 | ], 17 | } 18 | -------------------------------------------------------------------------------- /src-js/index.js: -------------------------------------------------------------------------------- 1 | export default {} 2 | -------------------------------------------------------------------------------- /src-php/Config/novapages.php: -------------------------------------------------------------------------------- 1 | [], 5 | 'replaceRepeaters' => false, 6 | 'homepageSlug' => 'homepage', 7 | 'models' => [ 8 | 'page' => 'Dewsign\NovaPages\Models\Page', 9 | ], 10 | 'resources' => [ 11 | 'page' => 'Dewsign\NovaPages\Nova\Page', 12 | ], 13 | 'group' => 'Pages', 14 | 'images' => [ 15 | 'disabled' => false, 16 | 'field' => 'Laravel\Nova\Fields\Image', 17 | 'disk' => 'public', 18 | ], 19 | 'videos' => [ 20 | 'disabled' => true, 21 | 'field' => 'Laravel\Nova\Fields\File', 22 | 'disk' => 'public', 23 | ], 24 | 'domainMap' => [], 25 | 'rootDomain' => env('ROOT_DOMAIN', config('session.domain')), 26 | 'enableLanguageSelection' => false, 27 | 'defaultLanguage' => [ 28 | 'en-GB' => 'English' 29 | ], 30 | 'languages' => [ 31 | 'de' => 'German', 32 | 'nl' => 'Dutch', 33 | 'fr' => 'French', 34 | 'it' => 'Italian', 35 | 'es' => 'Spanish' 36 | ] 37 | ]; 38 | -------------------------------------------------------------------------------- /src-php/Database/factories/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dewsign/nova-pages/1a00ef922dc78c8b5b682d9c532b55a64f5e7e23/src-php/Database/factories/.gitignore -------------------------------------------------------------------------------- /src-php/Database/factories/PageFactory.php: -------------------------------------------------------------------------------- 1 | define(config('novapages.models.page', Page::class), function (Faker $faker) { 8 | return [ 9 | 'active' => $faker->boolean(90), 10 | 'featured' => $faker->boolean(20), 11 | 'name' => $name = "{$faker->company}", 12 | 'slug' => Str::slug($name), 13 | 'image' => $faker->boolean(80) ? $faker->imageUrl($width = 640, $height = 480, 'business') : null, 14 | 'summary' => $faker->realText(rand(70, 500)), 15 | 'priority' => $faker->numberBetween(1, 100), 16 | ]; 17 | }); 18 | -------------------------------------------------------------------------------- /src-php/Database/migrations/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dewsign/nova-pages/1a00ef922dc78c8b5b682d9c532b55a64f5e7e23/src-php/Database/migrations/.gitignore -------------------------------------------------------------------------------- /src-php/Database/migrations/2018_10_09_000000_create_pages_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->active(); 19 | $table->featured(); 20 | $table->string('template')->nullable(); 21 | $table->string('name')->nullable(); 22 | $table->string('slug')->index(); 23 | $table->unsignedInteger('parent_id')->nullable(); 24 | $table->string('image')->nullable(); 25 | $table->text('summary')->nullable(); 26 | $table->priority(); 27 | $table->meta(); 28 | $table->timestamps(); 29 | }); 30 | 31 | Schema::table('pages', function (Blueprint $table) { 32 | $table->foreign('parent_id')->references('id')->on('pages')->onDelete('set null'); 33 | }); 34 | } 35 | 36 | /** 37 | * Reverse the migrations. 38 | * 39 | * @return void 40 | */ 41 | public function down() 42 | { 43 | Schema::dropIfExists('pages'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src-php/Database/migrations/2019_11_06_000000_update_pages_table_add_language.php: -------------------------------------------------------------------------------- 1 | string('language')->default(array_key_first(config('novapages.defaultLanguage')))->after('template'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('pages', function (Blueprint $table) { 29 | $table->dropColumn('language'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src-php/Database/migrations/2019_11_18_000000_update_pages_table_add_video.php: -------------------------------------------------------------------------------- 1 | string('video')->nullable()->after('image'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('pages', function (Blueprint $table) { 29 | $table->dropColumn('video'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src-php/Database/migrations/2020_01_01_000000_update_pages_table_add_alternative_text.php: -------------------------------------------------------------------------------- 1 | string('alternative_text')->nullable()->after('image'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function down() 27 | { 28 | Schema::table('pages', function (Blueprint $table) { 29 | $table->dropColumn('alternative_text'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src-php/Database/seeds/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dewsign/nova-pages/1a00ef922dc78c8b5b682d9c532b55a64f5e7e23/src-php/Database/seeds/.gitignore -------------------------------------------------------------------------------- /src-php/Database/seeds/PageSeeder.php: -------------------------------------------------------------------------------- 1 | create()->each(function ($page) { 21 | $page->repeaters()->saveMany(factory(Repeater::class, rand(0, 5))->create()->each(function ($repeater) { 22 | $repeater->type()->associate(factory(AvailableBlocks::random()::$model)->create())->save(); 23 | })); 24 | }); 25 | 26 | config('novapages.models.page', Page::class)::inRandomOrder()->take(rand(25, 75))->get()->each(function ($page) { 27 | $page->parent()->associate(config('novapages.models.page', Page::class)::inRandomOrder()->where('id', '<>', $page->id)->first()); 28 | $page->save(); 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src-php/Events/NovaPagesProviderRegistered.php: -------------------------------------------------------------------------------- 1 | filter()->implode('/'); 43 | } 44 | 45 | if ($desiredRoute = config('novapages.models.page', Page::class)::isWithinDomainMap($domain, $path)) { 46 | if ($desiredRoute !== URL::current()) { 47 | return Redirect::away($desiredRoute); 48 | } 49 | }; 50 | 51 | $page = app(config('novapages.models.page', Page::class))::withParent() 52 | ->whereFullPath($reverseMappedPath ?? $path, $excludeMappedDomains = false) 53 | ->first(); 54 | 55 | if (!$page) { 56 | abort(404, __('Page not found"')); 57 | } 58 | 59 | $page->authoriseToView(); 60 | 61 | return View::first([ 62 | $page->template ? "nova-pages::templates.{$page->template}" : null, 63 | 'nova-pages::show', 64 | ]) 65 | ->with('page', $page) 66 | ->whenActive($page); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src-php/Http/Middleware/RedirectHomepageSlugToRoot.php: -------------------------------------------------------------------------------- 1 | is($homepageSlug)) { 19 | return redirect('/', 301); 20 | } 21 | 22 | return $next($request); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src-php/Http/Middleware/ServePages.php: -------------------------------------------------------------------------------- 1 | domainMappedFolders = config('novapages.domainMap'); 61 | $this->homepageSlug = config('novapages.models.page')::getHomepageSlug(); 62 | 63 | parent::__construct($attributes); 64 | } 65 | 66 | /** 67 | * Get a page's parent 68 | * 69 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 70 | */ 71 | public function parent() 72 | { 73 | return $this->belongsTo(config('novapages.models.page', Page::class)); 74 | } 75 | 76 | /** 77 | * Returns the slug that should be used for the homepage. 78 | * 79 | * @return string 80 | */ 81 | public static function getHomepageSlug() 82 | { 83 | return config('novapages.homepageSlug', 'homepage'); 84 | } 85 | 86 | /** 87 | * Get a page's children. 88 | * 89 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 90 | */ 91 | public function children() 92 | { 93 | return $this->hasMany(config('novapages.models.page', Page::class), 'parent_id', 'id'); 94 | } 95 | 96 | /** 97 | * Add required items to the breadcrumb seed 98 | * 99 | * @return array 100 | */ 101 | public function seeds() 102 | { 103 | $trail = collect([]); 104 | 105 | $this->seedParent($trail, $this); 106 | 107 | return array_merge(parent::seeds(), $trail->all(), [ 108 | [ 109 | 'name' => $this->h1, 110 | 'url' => $this->mapped_url, 111 | ], 112 | ]); 113 | } 114 | 115 | /** 116 | * Check if the user is authorised to view the page 117 | * 118 | * @return void 119 | */ 120 | public function authoriseToView() 121 | { 122 | abort_if(Gate::denies('accessContent', $this), 403); 123 | } 124 | 125 | /** 126 | * Recursively add parent pages to the breadcrumb seed 127 | * 128 | * @param Illuminate\Support\Collection $seed 129 | * @param Dewsign\NovaPages\Models\Page $item 130 | * @return Illuminate\Support\Collection 131 | */ 132 | private function seedParent(&$seed, $item) 133 | { 134 | if (!$parent = $item->parent) { 135 | return; 136 | } 137 | 138 | $this->seedParent($seed, $parent); 139 | 140 | $seed->push([ 141 | 'name' => $parent->navTitle, 142 | 'url' => $parent->mapped_url, 143 | ]); 144 | } 145 | 146 | /** 147 | * The default canonical to be used on pages. 148 | * Returns a full url including any domain map. 149 | * 150 | * @return string 151 | */ 152 | public function baseCanonical() 153 | { 154 | return $this->mapped_url; 155 | } 156 | 157 | /** 158 | * Overload default method to exclude the homepage slug from the full path 159 | * 160 | * @return string Full path 161 | */ 162 | public function getFullPathAttribute() 163 | { 164 | $pathSections = explode('/', $this->getFullPath()); 165 | 166 | $slugsToExclude = array_merge( 167 | Arr::wrap($this->domainMappedFolders), 168 | Arr::wrap($this->homepageSlug) 169 | ); 170 | 171 | $finalSlug = collect($pathSections) 172 | ->filter() 173 | ->reject(function ($slug) use ($slugsToExclude) { 174 | return in_array($slug, $slugsToExclude); 175 | }) 176 | ->implode('/'); 177 | 178 | return Str::start($finalSlug, '/'); 179 | } 180 | 181 | /** 182 | * Returns the full URL of the page mapped to sub-domains where appropriate. 183 | * 184 | * @return string 185 | */ 186 | public function getMappedUrlAttribute() 187 | { 188 | $pathSections = collect(explode('/', $this->getFullPath()))->filter(); 189 | $mappedFolders = collect(Arr::wrap($this->domainMappedFolders)); 190 | 191 | if ($mappedFolders->contains($pathSections->first())) { 192 | return route('domain.pages.show', [ 193 | 'domain' => $pathSections->first(), 194 | 'path' => $this->full_path, 195 | ]); 196 | } 197 | 198 | return route('pages.show', [ 199 | 'path' => $this->full_path, 200 | ]); 201 | } 202 | 203 | /** 204 | * Overload scope to add additional cnditions for the homepage slug 205 | * 206 | * @param \Illuminate\Database\Eloquent\Builder $query 207 | * @param string $path Full path 208 | * @return \Illuminate\Database\Eloquent\Builder 209 | */ 210 | public function scopeWhereFullPath(Builder $query, string $path, bool $excludeMappedDomains = true) 211 | { 212 | $itemSlugs = explode('/', $path); 213 | 214 | $slugsToExclude = Arr::wrap($this->homepageSlug); 215 | 216 | $finalSlug = collect($itemSlugs) 217 | ->filter() 218 | ->reject(function ($slug) use ($slugsToExclude, $excludeMappedDomains) { 219 | if (!$excludeMappedDomains) { 220 | return false; 221 | } 222 | 223 | return in_array($slug, $slugsToExclude); 224 | }) 225 | ->implode('/'); 226 | 227 | return $query->where('slug', '=', end($itemSlugs)) 228 | ->get() 229 | ->filter(function ($item) use ($finalSlug, $excludeMappedDomains) { 230 | if (!$excludeMappedDomains) { 231 | return $item->getFullPath() === Str::start($finalSlug, '/'); 232 | } 233 | 234 | return $item->full_path === Str::start($finalSlug, '/'); 235 | }); 236 | } 237 | 238 | /** 239 | * Return a page object to allow customising of meta fields for non-dynamic pages. 240 | * E.g. blog index page. Pass in a default string or array incase no matching page exists. 241 | * 242 | * @param string $slug 243 | * @param string|array $default 244 | * @return Collection|array 245 | */ 246 | public static function meta(string $slug, $default = null) 247 | { 248 | if (!is_array($default)) { 249 | $default = [ 250 | 'page_title' => $default, 251 | 'browser_title' => $default, 252 | 'meta_description' => $default, 253 | 'h1' => $default, 254 | ]; 255 | } 256 | 257 | return self::withParent()->whereFullPath($slug)->first() ?? $default; 258 | } 259 | 260 | 261 | /** 262 | * Checks to see if the first part of the path is within the domain map. 263 | * Returns false or the full url of the desired page. 264 | * 265 | * @return array 266 | */ 267 | public static function isWithinDomainMap($domain = null, $path = '') 268 | { 269 | if (!$page = static::withParent()->whereFullPath($path, $excludeMappedDomain = false)->first()) { 270 | return false; 271 | }; 272 | 273 | if ($page->full_path !== $page->getFullPath()) { 274 | return $page->mapped_url; 275 | }; 276 | 277 | return false; 278 | } 279 | 280 | public function formatLanguageCode() 281 | { 282 | if ($this->language === null) { 283 | return Str::after(array_key_first(config('novapages.defaultLanguage')), '-'); 284 | } 285 | 286 | if (Str::contains($this->language, '-')) { 287 | return Str::after($this->language, '-'); 288 | } 289 | 290 | return $this->language; 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src-php/Nova/Filters/ActiveState.php: -------------------------------------------------------------------------------- 1 | whereActive($value); 21 | } 22 | 23 | /** 24 | * Get the filter's available options. 25 | * 26 | * @param \Illuminate\Http\Request $request 27 | * @return array 28 | */ 29 | public function options(Request $request) 30 | { 31 | return [ 32 | 'Active' => true, 33 | 'Inactive' => false, 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src-php/Nova/Filters/PageType.php: -------------------------------------------------------------------------------- 1 | whereDoesntHave('parent'); 22 | } 23 | 24 | if ($value === 'parent') { 25 | return $query->whereHas('children'); 26 | } 27 | 28 | if ($value === 'child') { 29 | return $query->whereDoesntHave('children'); 30 | } 31 | } 32 | 33 | /** 34 | * Get the filter's available options. 35 | * 36 | * @param \Illuminate\Http\Request $request 37 | * @return array 38 | */ 39 | public function options(Request $request) 40 | { 41 | return [ 42 | 'Root Pages' => 'root', 43 | 'Parent Pages' => 'parent', 44 | 'Final Child Pages' => 'child', 45 | ]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src-php/Nova/NovaPagesTool.php: -------------------------------------------------------------------------------- 1 | sortable(), 83 | $this->templateOptions(), 84 | Boolean::make('Active')->sortable()->rules('required', 'boolean'), 85 | Boolean::make('Featured')->sortable()->rules('required', 'boolean'), 86 | Number::make('Priority')->sortable()->rules('required', 'integer'), 87 | $this->languageOptions(), 88 | TextWithSlug::make('Name')->sortable()->rules('required_if:active,1', 'max:254')->slug('slug'), 89 | Slug::make('Slug')->sortable()->rules('required', 'alpha_dash', 'max:254')->hideFromIndex(), 90 | BelongsTo::make('Parent', 'parent', self::class)->nullable()->searchable()->rules('not_in:{{resourceId}}')->singularLabel('Parent'), 91 | Text::make('Full Path', function () { 92 | return $this->mapped_url; 93 | })->hideFromIndex(), 94 | $this->imageField(), 95 | $this->videoField(), 96 | Textarea::make('Summary'), 97 | HasMany::make('Child Pages', 'children', self::class), 98 | MorphMany::make(__('Repeaters'), 'repeaters', PageRepeaters::class), 99 | MetaAttributes::make(), 100 | AccessControl::make(), 101 | ]; 102 | } 103 | 104 | private function templateOptions() 105 | { 106 | $options = NovaPages::availableTemplates(); 107 | 108 | if (count($options) <= 1) { 109 | return $this->merge([]); 110 | } 111 | 112 | return $this->merge([ 113 | Select::make('Template') 114 | ->options($options) 115 | ->displayUsingLabels() 116 | ->hideFromIndex(), 117 | ]); 118 | } 119 | 120 | /** 121 | * Show the video field if enabled in the config 122 | * 123 | * @return mixed 124 | */ 125 | private function videoField() 126 | { 127 | if (config('novapages.videos.disabled')) { 128 | return $this->merge([]); 129 | } 130 | 131 | return $this->merge([ 132 | config('novapages.videos.field')::make('Video')->disk(config('novapages.videos.disk', 'public')), 133 | ]); 134 | } 135 | 136 | /** 137 | * Show the image field if enabled in the config 138 | * 139 | * @return mixed 140 | */ 141 | private function imageField() 142 | { 143 | if (config('novapages.images.disabled')) { 144 | return $this->merge([]); 145 | } 146 | 147 | return $this->merge([ 148 | config('novapages.images.field')::make('Image')->disk(config('novapages.images.disk', 'public')), 149 | Text::make('Alternative Text')->rules('nullable', 'max:254')->hideFromIndex(), 150 | ]); 151 | } 152 | 153 | private function languageOptions() 154 | { 155 | if (!config('novapages.enableLanguageSelection')) { 156 | return $this->merge([]); 157 | } 158 | 159 | $options = NovaPages::availableLanguages(); 160 | 161 | return $this->merge([ 162 | Text::make('Language', function () { 163 | return country($this->formatLanguageCode())->getEmoji(); 164 | }), 165 | Select::make(__('Language'), 'language') 166 | ->options($options) 167 | ->displayUsingLabels() 168 | ->hideFromIndex(), 169 | ]); 170 | } 171 | 172 | /** 173 | * Get the cards available for the request. 174 | * 175 | * @param \Illuminate\Http\Request $request 176 | * @return array 177 | */ 178 | public function cards(Request $request) 179 | { 180 | return []; 181 | } 182 | 183 | /** 184 | * Get the filters available for the resource. 185 | * 186 | * @param \Illuminate\Http\Request $request 187 | * @return array 188 | */ 189 | public function filters(Request $request) 190 | { 191 | return [ 192 | new ActiveState, 193 | new PageType, 194 | ]; 195 | } 196 | 197 | /** 198 | * Get the lenses available for the resource. 199 | * 200 | * @param \Illuminate\Http\Request $request 201 | * @return array 202 | */ 203 | public function lenses(Request $request) 204 | { 205 | return []; 206 | } 207 | 208 | /** 209 | * Get the actions available for the resource. 210 | * 211 | * @param \Illuminate\Http\Request $request 212 | * @return array 213 | */ 214 | public function actions(Request $request) 215 | { 216 | return []; 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src-php/Nova/PageRepeaters.php: -------------------------------------------------------------------------------- 1 | get()->map(function ($item) use ($sitemap) { 14 | $sitemap->add(url($item->mapped_url)); 15 | }); 16 | } 17 | 18 | public static function availableTemplates() 19 | { 20 | $packageTemplatePath = __DIR__ . '/Resources/views/templates'; 21 | $appTemplatePath = resource_path() . '/views/vendor/nova-pages/templates'; 22 | 23 | $packageTemplates = File::exists($packageTemplatePath) ? File::files($packageTemplatePath) : null; 24 | $appTemplates = File::exists($appTemplatePath) ? File::files($appTemplatePath) : null; 25 | 26 | return collect($packageTemplates)->merge($appTemplates)->mapWithKeys(function ($file) { 27 | $filename = $file->getFilename(); 28 | 29 | return [ 30 | str_replace('.blade.php', '', $filename) => static::getPrettyFilename($filename), 31 | ]; 32 | })->all(); 33 | } 34 | 35 | public static function availableLanguages() 36 | { 37 | $default = config('novapages.defaultLanguage'); 38 | $languages = config('novapages.languages'); 39 | 40 | $options = array_merge($default, $languages); 41 | 42 | return collect($options)->map(function ($item) { 43 | return __($item); 44 | }); 45 | } 46 | 47 | private static function getPrettyFilename($filename) 48 | { 49 | $basename = str_replace('.blade.php', '', $filename); 50 | 51 | return Str::title(str_replace('-', ' ', $basename)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src-php/Policies/PagePolicy.php: -------------------------------------------------------------------------------- 1 | can('managePage'); 25 | } 26 | 27 | public function update($user, $model) 28 | { 29 | return $user->can('managePage', $model); 30 | } 31 | 32 | public function delete($user, $model) 33 | { 34 | return $user->can('managePage', $model); 35 | } 36 | 37 | public function restore($user, $model) 38 | { 39 | return $user->can('managePage', $model); 40 | } 41 | 42 | public function forceDelete($user, $model) 43 | { 44 | return $user->can('managePage', $model); 45 | } 46 | 47 | public function viewInactive($user = null, $page) 48 | { 49 | if (config('maxfactor-support.canViewInactive')) { 50 | return true; 51 | } 52 | 53 | if ($page->active) { 54 | return true; 55 | } 56 | 57 | if (Gate::allows('viewNova')) { 58 | return true; 59 | } 60 | 61 | return false; 62 | } 63 | 64 | public function accessContent($user = null, $page) 65 | { 66 | $page = $this->getFirstPageWithAccessRoles($page); 67 | 68 | if ($page->access === null) { 69 | return true; 70 | } 71 | 72 | if (!count($page->access->roles)) { 73 | return true; 74 | } 75 | 76 | if ($user === null) { 77 | return false; 78 | } 79 | 80 | return $user->roles->pluck('id')->intersect($page->access->roles)->count(); 81 | } 82 | 83 | /** 84 | * Find the first page up the parent tree with access roles. 85 | * 86 | * @param [type] $page 87 | * @return Page 88 | */ 89 | private function getFirstPageWithAccessRoles($page) 90 | { 91 | if ($page->roles) { 92 | return $page; 93 | } 94 | 95 | if (!$page->parent) { 96 | return $page; 97 | } 98 | 99 | return $this->getFirstPageWithAccessRoles($page->parent); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src-php/Providers/AuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | PagePolicy::class, 22 | ]; 23 | 24 | /** 25 | * Register any authentication / authorization services. 26 | * 27 | * @return void 28 | */ 29 | public function boot() 30 | { 31 | collect([ 32 | 'viewPage', 33 | 'managePage', 34 | ])->each(function ($permission) { 35 | Gate::define($permission, function ($user) use ($permission) { 36 | if ($this->nobodyHasAccess($permission)) { 37 | return true; 38 | } 39 | 40 | return $user->hasRoleWithPermission($permission); 41 | }); 42 | }); 43 | 44 | $this->registerPolicies(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src-php/Providers/PackageServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishConfigs(); 29 | $this->bootViews(); 30 | $this->bootAssets(); 31 | $this->bootCommands(); 32 | $this->publishDatabaseFiles(); 33 | $this->registerMorphMaps(); 34 | $this->configurePagination(); 35 | $this->loadTranslations(); 36 | $this->bootRoutes($router); 37 | } 38 | 39 | /** 40 | * Register the application services. 41 | * 42 | * @return void 43 | */ 44 | public function register() 45 | { 46 | Nova::resources([ 47 | PageRepeaters::class, 48 | ]); 49 | 50 | $this->mergeConfigFrom( 51 | $this->getConfigsPath(), 52 | 'novapages' 53 | ); 54 | } 55 | 56 | /** 57 | * Publish configuration file. 58 | * 59 | * @return void 60 | */ 61 | private function publishConfigs() 62 | { 63 | $this->publishes([ 64 | $this->getConfigsPath() => config_path('novapages.php'), 65 | ], 'config'); 66 | } 67 | 68 | /** 69 | * Get local package configuration path. 70 | * 71 | * @return string 72 | */ 73 | private function getConfigsPath() 74 | { 75 | return __DIR__.'/../Config/novapages.php'; 76 | } 77 | 78 | /** 79 | * Register the artisan packages' terminal commands 80 | * 81 | * @return void 82 | */ 83 | private function bootCommands() 84 | { 85 | if ($this->app->runningInConsole()) { 86 | $this->commands([ 87 | // MyCommand::class, 88 | ]); 89 | } 90 | } 91 | 92 | /** 93 | * Load custom views 94 | * 95 | * @return void 96 | */ 97 | private function bootViews() 98 | { 99 | $this->loadViewsFrom(__DIR__.'/../Resources/views', 'nova-pages'); 100 | $this->publishes([ 101 | __DIR__.'/../Resources/views' => resource_path('views/vendor/nova-pages'), 102 | ]); 103 | } 104 | 105 | /** 106 | * Define publishable assets 107 | * 108 | * @return void 109 | */ 110 | private function bootAssets() 111 | { 112 | $this->publishes([ 113 | __DIR__.'/../Resources/assets/js' => resource_path('assets/js/vendor/nova-pages'), 114 | ], 'js'); 115 | } 116 | 117 | private function publishDatabaseFiles() 118 | { 119 | $this->loadMigrationsFrom(__DIR__.'/../Database/migrations'); 120 | 121 | $this->loadFactories(); 122 | 123 | $this->publishes([ 124 | __DIR__ . '/../Database/factories' => base_path('database/factories') 125 | ], 'factories'); 126 | 127 | $this->publishes([ 128 | __DIR__ . '/../Database/migrations' => base_path('database/migrations') 129 | ], 'migrations'); 130 | 131 | $this->publishes([ 132 | __DIR__ . '/../Database/seeds' => base_path('database/seeds') 133 | ], 'seeds'); 134 | } 135 | 136 | /** 137 | * Only load the factories in non-production ready environments 138 | * 139 | * @return void 140 | */ 141 | public function loadFactories() 142 | { 143 | if (App::environment(['production', 'staging'])) { 144 | return; 145 | } 146 | 147 | $this->app->make('Illuminate\Database\Eloquent\Factory')->load( 148 | __DIR__ . '/../Database/factories' 149 | ); 150 | } 151 | 152 | /** 153 | * Register the Mophmaps 154 | * 155 | * @return void 156 | */ 157 | private function registerMorphmaps() 158 | { 159 | Relation::morphMap([ 160 | 'novapages.page' => config('novapages.models.page', Page::class), 161 | ]); 162 | } 163 | 164 | /** 165 | * Set te default pagination to not use bootstrap markup 166 | * 167 | * @return void 168 | */ 169 | private function configurePagination() 170 | { 171 | Paginator::defaultView('pagination::default'); 172 | } 173 | 174 | private function loadTranslations() 175 | { 176 | $this->loadJSONTranslationsFrom(__DIR__.'/../Resources/lang', 'novapages'); 177 | } 178 | 179 | /** 180 | * Ensure the catch-all routes for pages are loaded after all other routes 181 | * 182 | * @return void 183 | */ 184 | private function bootRoutes(Router $router) 185 | { 186 | Event::listen(NovaPagesProviderRegistered::class, function () { 187 | $this->app->register(RouteServiceProvider::class); 188 | }); 189 | 190 | if ($this->app->runningInConsole()) { 191 | $this->app->register(RouteServiceProvider::class); 192 | } 193 | 194 | $this->app->make(HttpKernel::class) 195 | ->pushMiddleware(ServePages::class); 196 | 197 | $router->pushMiddlewareToGroup('web', RedirectHomepageSlugToRoot::class); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src-php/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | namespace($this->namespace) 40 | ->group(__DIR__ . '/../Routes/web.php'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src-php/Resources/assets/js/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dewsign/nova-pages/1a00ef922dc78c8b5b682d9c532b55a64f5e7e23/src-php/Resources/assets/js/.gitignore -------------------------------------------------------------------------------- /src-php/Resources/lang/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "viewPage": "View Pages", 3 | "managePage": "Manage Pages" 4 | } 5 | -------------------------------------------------------------------------------- /src-php/Resources/views/show.blade.php: -------------------------------------------------------------------------------- 1 | @repeaterblocks($page) 2 | -------------------------------------------------------------------------------- /src-php/Resources/views/templates/default.blade.php: -------------------------------------------------------------------------------- 1 | @include('nova-pages::show') 2 | -------------------------------------------------------------------------------- /src-php/Routes/pages.php: -------------------------------------------------------------------------------- 1 | name('pages.show') 7 | ->where(['path' => '^(?!' . $nova_url . '|nova-api|nova-vendor).*']) 8 | ->defaults('domain', '') 9 | ->defaults('path', config('novapages.models.page')::getHomepageSlug()); 10 | -------------------------------------------------------------------------------- /src-php/Routes/web.php: -------------------------------------------------------------------------------- 1 | name('domain.')->group(function () { 7 | include('pages.php'); 8 | }); 9 | 10 | include('pages.php'); 11 | -------------------------------------------------------------------------------- /src-php/Tests/PageTest.php: -------------------------------------------------------------------------------- 1 | get(route('pages.show', [ 15 | app(config('novapages.models.page', Page::class))::active() 16 | ->doesntHave('parent') 17 | ->inRandomOrder() 18 | ->first() 19 | ->full_path, 20 | ])); 21 | 22 | $result->assertOk(); 23 | } 24 | 25 | public function testRandomChildPageIsAccessible() 26 | { 27 | $result = $this->get(route('pages.show', [ 28 | app(config('novapages.models.page', Page::class))::active() 29 | ->has('parent') 30 | ->inRandomOrder() 31 | ->first() 32 | ->full_path, 33 | ])); 34 | 35 | $result->assertOk(); 36 | } 37 | } 38 | --------------------------------------------------------------------------------