commit e534ddd14c410e2528f82bfaf0511b49e797c6dc Author: Samuel Štancl Date: Fri Aug 6 04:31:37 2021 +0200 add source code diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..619007e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +/.github export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore + +/docker-compose.yml export-ignore +/tests export-ignore + +/.php_cs.php export-ignore +/psalm.xml export-ignore +/phpunit.xml export-ignore +/check export-ignore +/coverage export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2ea3836 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: CI + +env: + COMPOSE_INTERACTIVE_NO_CLI: 1 + PHP_CS_FIXER_IGNORE_ENV: 1 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +on: + push: + pull_request: + branches: [ master ] + +jobs: + pest: + name: Tests (Pest) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Install composer dependencies + run: composer install + - name: Run tests + run: vendor/bin/pest + + php-cs-fixer: + name: Code style (php-cs-fixer) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install php-cs-fixer + run: composer global require friendsofphp/php-cs-fixer + - name: Run php-cs-fixer + run: $HOME/.composer/vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php + - name: Commit changes from php-cs-fixer + uses: EndBug/add-and-commit@v5 + with: + author_name: "PHP CS Fixer" + author_email: "phpcsfixer@example.com" + message: Fix code style (php-cs-fixer) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a683006 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.phpunit.result.cache +package-lock.json +composer.lock +vendor/ +.php-cs-fixer.cache +.vscode/ +coverage/ +node_modules diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..589838b --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,141 @@ + ['syntax' => 'short'], + 'binary_operator_spaces' => [ + 'default' => 'single_space', + 'operators' => [ + '=>' => null, + '|' => 'no_space', + ] + ], + 'blank_line_after_namespace' => true, + 'blank_line_after_opening_tag' => true, + 'no_superfluous_phpdoc_tags' => true, + 'blank_line_before_statement' => [ + 'statements' => ['return'] + ], + 'braces' => true, + 'cast_spaces' => true, + 'class_definition' => true, + 'concat_space' => [ + 'spacing' => 'one' + ], + 'declare_equal_normalize' => true, + 'elseif' => true, + 'encoding' => true, + 'full_opening_tag' => true, + 'declare_strict_types' => true, + 'fully_qualified_strict_types' => true, // added by Shift + 'function_declaration' => true, + 'function_typehint_space' => true, + 'heredoc_to_nowdoc' => true, + 'include' => true, + 'increment_style' => ['style' => 'post'], + 'indentation_type' => true, + 'linebreak_after_opening_tag' => true, + 'line_ending' => true, + 'lowercase_cast' => true, + 'constant_case' => true, + 'lowercase_keywords' => true, + 'lowercase_static_reference' => true, // added from Symfony + 'magic_method_casing' => true, // added from Symfony + 'magic_constant_casing' => true, + 'method_argument_space' => true, + 'native_function_casing' => true, + 'no_alias_functions' => true, + 'no_extra_blank_lines' => [ + 'tokens' => [ + 'extra', + 'throw', + 'use', + 'use_trait', + ] + ], + 'no_blank_lines_after_class_opening' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_closing_tag' => true, + 'no_empty_phpdoc' => true, + 'no_empty_statement' => true, + 'no_leading_import_slash' => true, + 'no_leading_namespace_whitespace' => true, + 'no_mixed_echo_print' => [ + 'use' => 'echo' + ], + 'no_multiline_whitespace_around_double_arrow' => true, + 'multiline_whitespace_before_semicolons' => [ + 'strategy' => 'no_multi_line' + ], + 'no_short_bool_cast' => true, + 'no_singleline_whitespace_before_semicolons' => true, + 'no_spaces_after_function_name' => true, + 'no_spaces_around_offset' => true, + 'no_spaces_inside_parenthesis' => true, + 'no_trailing_comma_in_list_call' => true, + 'no_trailing_comma_in_singleline_array' => true, + 'no_trailing_whitespace' => true, + 'no_trailing_whitespace_in_comment' => true, + 'no_unneeded_control_parentheses' => true, + 'no_unreachable_default_argument_value' => true, + 'no_useless_return' => true, + 'no_whitespace_before_comma_in_array' => true, + 'no_whitespace_in_blank_line' => true, + 'normalize_index_brace' => true, + 'not_operator_with_successor_space' => true, + 'object_operator_without_whitespace' => true, + 'ordered_imports' => ['sort_algorithm' => 'alpha'], + 'phpdoc_indent' => true, + 'general_phpdoc_tag_rename' => true, + 'phpdoc_no_access' => true, + 'phpdoc_no_package' => true, + 'phpdoc_no_useless_inheritdoc' => true, + 'phpdoc_scalar' => true, + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_summary' => true, + 'phpdoc_to_comment' => false, + 'phpdoc_trim' => true, + 'phpdoc_types' => true, + 'phpdoc_var_without_name' => true, + 'psr_autoloading' => true, + 'self_accessor' => true, + 'short_scalar_cast' => true, + 'simplified_null_return' => false, // disabled by Shift + 'single_blank_line_at_eof' => true, + 'single_blank_line_before_namespace' => true, + 'single_class_element_per_statement' => true, + 'single_import_per_statement' => true, + 'single_line_after_imports' => true, + 'no_unused_imports' => true, + 'single_line_comment_style' => [ + 'comment_types' => ['hash'] + ], + 'single_quote' => true, + 'space_after_semicolon' => true, + 'standardize_not_equals' => true, + 'switch_case_semicolon_to_colon' => true, + 'switch_case_space' => true, + 'ternary_operator_spaces' => true, + 'trailing_comma_in_multiline' => true, + 'trim_array_spaces' => true, + 'unary_operator_spaces' => true, + 'whitespace_after_comma_in_array' => true, +]; + +$project_path = getcwd(); +$finder = Finder::create() + ->in([ + $project_path . '/src', + ]) + ->name('*.php') + ->notName('*.blade.php') + ->ignoreDotFiles(true) + ->ignoreVCS(true); + +return (new Config()) + ->setFinder($finder) + ->setRules($rules) + ->setRiskyAllowed(true) + ->setUsingCache(true); diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b285610 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 ArchTech Development, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3c1286d --- /dev/null +++ b/README.md @@ -0,0 +1,116 @@ +# Laravel Pages + +This package lets you create pages using Markdown or Blade without having to worry about creating routes or controllers yourself. + +Essentially, you create either `content/pages/foo.md` or `resources/views/pages/foo.blade.php`. + +Markdown files use a pre-defined Blade view to get rendered. Blade files are meant for pages which don't follow the default layout and need more custom styling. + +For instance, you could have the `/pricing` route use a Blade file (`pages/pricing.blade.php`) with a pretty design that accompanies your pricing copy. + +Whereas for `/about`, you could have a simple Markdown file (`content/pages/about.md`) that describes your service using pure text without any special graphical elements. + +We use this on the ArchTech website — the [About](https://archte.ch/about), [Careers](https://archte.ch/careers), and [Open source](https://archte.ch/open-source) pages are simple Markdown files. + +## Installation + +Require the package via composer: + +``` +composer require archtechx/laravel-pages +``` + +Publish the config file: + +``` +php artisan vendor:publish --tag=archtech-pages-config +``` + +And finally, add this line to the **end** of your `routes/web.php` file: + +```php +ArchTech\Pages\Page::routes(); +``` + +This line will register the routes in a way that ensures that your routes take precedence, and the page route is only used as the final option. + +## Usage + +### Markdown pages + +To create a markdown file, create a file in `content/pages/`. The route to the page will match the file name (without `.md`). + +For example, to create the `/about` page, create `content/pages/about.md` with this content: + +```md +--- +slug: about +title: 'About us' +updated_at: 2021-05-19T19:09:02+00:00 +created_at: 2021-05-19T19:09:02+00:00 +--- + +We are a web development agency that specializes in ... +``` + +### Blade pages + +To create a Blade page, create a file in `resources/views/pages/`. Like in the Markdown example, the route to the page will match the file name without the extension. + +Therefore to create the `/about` page, you'd create `resources/views/pages/about.blade.php`: + +```html + + This view can use any layouts or markup. + +``` + +## Configuration + +You'll likely want to configure a few things, most likely the used layout. + +To do that, simply modify `config/pages.php`. + +The config file lets you change: +- the used model +- the used controller +- the layout used by the markdown views +- the view file used to render Markdown pages +- routing details + +The layout is used *by* the vendor (package-provided) Markdown view. You'll likely want to set it to something like ``app-layout` or `layouts.app`.` + +If you'd like to change the file that renders the Markdown itself, create `resources/views/pages/_markdown.blade.php` (the `_` prefix is important as it prevents direct visits) and change the `pages.views.markdown` config key to `pages._markdown`. + +And if you'd like to customize the routing logic more ethan the config file allows you, simply register the route yourself (instead of calling `Page::routes()`): + +```php +Route::get('/{page}', ArchTech\Pages\PageController::class); +``` + +## Ecosystem support + +The package perfectly supports other tools in the ecosystem, such as [Laravel Nova](https://nova.laravel.com) or [Lean Admin](https://lean-admin.dev). + +For example, in Laravel Nova you could create a resource for the package-provided `Page` model (`ArchTech\Pages\Page`) and use the following field schema: + +```php +public function fields(Request $request) +{ + return [ + Text::make('slug'), + Text::make('title'), + Markdown::make('content'), + ]; +} +``` + +## Git integration & Orbit + +This package uses [Orbit](https://github.com/ryangjchandler/orbit) under the hood — to manage the Markdown files as Eloquent models. If you'd like to customize some things related to that logic, take a look at the Orbit documentation. + +The package also uses another package of ours, [Laravel SEO](https://github.com/archtechx/laravel-seo), to provide meta tag support for Markdown pages. We recommended that you use this package yourself, since it will make handling meta tags as easy as adding the following line to your layout's `` section: + +```html + +``` diff --git a/assets/config.php b/assets/config.php new file mode 100644 index 0000000..114d0fb --- /dev/null +++ b/assets/config.php @@ -0,0 +1,36 @@ + ArchTech\Pages\Page::class, + + 'views' => [ + /** + * The layout used to render the pages. + * + * @example app-layout For resources/views/app-layout.blade.php + * @example layouts.app For resources/views/layouts.app.blade.php + */ + 'layout' => 'app-layout', + + /** + * The path to your views. + * + * @example 'pages.' The package will look into resources/views/pages + * @example 'foo::' The package will look into the 'foo' view namespace + */ + 'path' => 'pages.', + + /** + * The name of the view used to render markdown pages. + * + * @example 'pages._markdown' The package will use resources/views/pages/_markdown.blade.php + */ + 'markdown' => 'pages::_markdown', + ], + + 'routes' => [ + 'name' => 'page', + 'prefix' => '', + 'handler' => ArchTech\Pages\PageController::class, + ], +]; diff --git a/assets/views/_markdown.blade.php b/assets/views/_markdown.blade.php new file mode 100644 index 0000000..3ee579a --- /dev/null +++ b/assets/views/_markdown.blade.php @@ -0,0 +1,9 @@ + +
+
+

{{ $page->title }}

+ + {!! Str::markdown($page->content) !!} +
+
+
diff --git a/check b/check new file mode 100755 index 0000000..305d51e --- /dev/null +++ b/check @@ -0,0 +1,47 @@ +#!/bin/bash +set -e + +offer_run() { + read -p "For more output, run $1. Run it now (Y/n)? " run + + case ${run:0:1} in + n|N ) + exit 1 + ;; + * ) + $1 + ;; + esac + + exit 1 +} + +if (php-cs-fixer fix --dry-run --config=.php-cs-fixer.php > /dev/null 2>/dev/null); then + echo '✅ php-cs-fixer OK' +else + read -p "⚠️ php-cs-fixer found issues. Fix (Y/n)? " fix + case ${fix:0:1} in + n|N ) + echo '❌ php-cs-fixer FAIL' + offer_run 'php-cs-fixer fix --config=.php-cs-fixer.php' + ;; + * ) + if (php-cs-fixer fix --config=.php-cs-fixer.php > /dev/null 2>/dev/null); then + echo '✅ php-cs-fixer OK' + else + echo '❌ php-cs-fixer FAIL' + offer_run 'php-cs-fixer fix --config=.php-cs-fixer.php' + fi + ;; + esac +fi + +if (./vendor/bin/pest > /dev/null 2>/dev/null); then + echo '✅ PEST OK' +else + echo '❌ PEST FAIL' + offer_run './vendor/bin/pest' +fi + +echo '==================' +echo '✅ Everything OK' diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..6f818b2 --- /dev/null +++ b/composer.json @@ -0,0 +1,42 @@ +{ + "name": "archtechx/laravel-pages", + "description": "", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Samuel Štancl", + "email": "samuel@archte.ch" + } + ], + "autoload": { + "psr-4": { + "ArchTech\\Pages\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "ArchTech\\Pages\\Tests\\": "tests/" + } + }, + "require": { + "php": "^8.0", + "illuminate/support": "^8.24", + "archtechx/laravel-seo": "^0.2.2", + "ryangjchandler/orbit": "^0.9.0", + "illuminate/routing": "^8.53", + "illuminate/database": "^8.53" + }, + "require-dev": { + "orchestra/testbench": "^6.9", + "pestphp/pest": "^1.2", + "pestphp/pest-plugin-laravel": "^1.0" + }, + "extra": { + "laravel": { + "providers": [ + "ArchTech\\Pages\\PackageServiceProvider" + ] + } + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..d26232e --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,33 @@ + + + + + ./src + + + + + + + + + ./tests + + + + + + + + + + + + + + + + + + + diff --git a/src/Page.php b/src/Page.php new file mode 100644 index 0000000..8a57666 --- /dev/null +++ b/src/Page.php @@ -0,0 +1,41 @@ +string('slug'); + $table->string('title'); + $table->longText('content'); + } + + public function getKeyName() + { + return 'slug'; + } + + public function getIncrementing() + { + return false; + } + + public static function routes(): void + { + Route::get('/{page}', config('pages.routes.handler')) + ->prefix(config('pages.routes.prefix')) + ->name(config('pages.routes.name')); + } +} diff --git a/src/PageController.php b/src/PageController.php new file mode 100644 index 0000000..b5c0555 --- /dev/null +++ b/src/PageController.php @@ -0,0 +1,33 @@ +exists($view)) { + return view($view); + } + + if ($model = config('pages.model')::find($page)) { + seo() + ->title($model->title) + ->description(Str::limit($model->content, 100)); + + return view(config('pages.views.markdown'), ['page' => $model]); + } + + abort(404); + } +} diff --git a/src/PagesServiceProvider.php b/src/PagesServiceProvider.php new file mode 100644 index 0000000..1ae0f60 --- /dev/null +++ b/src/PagesServiceProvider.php @@ -0,0 +1,28 @@ +loadViewsFrom(__DIR__ . '/../assets/views', 'pages'); + $this->mergeConfigFrom(__DIR__ . '/../assets/config.php', 'pages'); + + $this->publishes([ + __DIR__ . '/../assets/views' => resource_path('views/vendor/pages'), + ], 'archtech-pages-views'); + + $this->publishes([ + __DIR__ . '/../assets/config.php' => config_path('pages.php'), + ], 'archtech-pages-config'); + } +} diff --git a/tests/CreatesApplication.php b/tests/CreatesApplication.php new file mode 100644 index 0000000..547152f --- /dev/null +++ b/tests/CreatesApplication.php @@ -0,0 +1,22 @@ +make(Kernel::class)->bootstrap(); + + return $app; + } +} diff --git a/tests/Feature/PagesTest.php b/tests/Feature/PagesTest.php new file mode 100644 index 0000000..ba4feca --- /dev/null +++ b/tests/Feature/PagesTest.php @@ -0,0 +1,79 @@ +delete(); + + view()->addNamespace('test', __DIR__ . '/../views'); + + config([ + 'pages.views.layout' => 'test::layout', + 'pages.views.path' => 'test::', + 'orbit.paths.content' => __DIR__ . '/../orbit/content', + 'orbit.paths.cache' => __DIR__ . '/../orbit/cache', + ]); +}); + +test('a view is shown if it exists') + ->get('/example') + ->assertSee('Test view'); + +test('markdown is rendered if it exists', function () { + Page::create([ + 'slug' => 'test', + 'title' => 'Markdown page', + 'content' => 'This is a **test page**' + ]); + + using($this) + ->get('/test') + ->assertSee('Markdown page') + ->assertSee('test page', false); +}); + +test('view takes precedence over markdown', function () { + Page::create([ + 'slug' => 'example', + 'title' => 'Test page', + 'content' => 'This is a test page' + ]); + + using($this) + ->get('/example') + ->assertSee('Test view') + ->assertDontSee('Test page'); +}); + +test('404 is returned if no view or markdown is found') + ->get('/foo') + ->assertNotFound(); + +test('a custom layout can be used', function () { + config(['pages.views.layout' => 'test::layout2']); + + Page::create([ + 'slug' => 'test', + 'title' => 'Test page', + 'content' => 'This is a test page' + ]); + + using($this) + ->get('/test') + ->assertSee('second layout'); +}); + +test('SEO metadata is set on markdown pages', function () { + Page::create([ + 'slug' => 'test', + 'title' => 'Test page', + 'content' => 'This is a test page' + ]); + + using($this) + ->get('/test') + ->assertSee('', false) + ->assertSee('', false); +}); diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..3dfec07 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,47 @@ +in('Feature'); + +/* +|-------------------------------------------------------------------------- +| Expectations +|-------------------------------------------------------------------------- +| +| When you're writing tests, you often need to check that values meet certain conditions. The +| "expect()" function gives you access to a set of "expectations" methods that you can use +| to assert different things. Of course, you may extend the Expectation API at any time. +| +*/ + +expect()->extend('toBeOne', function () { + return $this->toBe(1); +}); + +/* +|-------------------------------------------------------------------------- +| Functions +|-------------------------------------------------------------------------- +| +| While Pest is very powerful out-of-the-box, you may have some testing code specific to your +| project that you don't want to repeat in every file. Here you can also expose helpers as +| global functions to help you to reduce the number of lines of code in your test files. +| +*/ + +function using($test): TestCase +{ + return $test; +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..f0784c3 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,20 @@ + + + {{ $slot }} + diff --git a/tests/views/components/layout2.blade.php b/tests/views/components/layout2.blade.php new file mode 100644 index 0000000..19910c7 --- /dev/null +++ b/tests/views/components/layout2.blade.php @@ -0,0 +1,3 @@ +
+ {{ $slot }} +
diff --git a/tests/views/example.blade.php b/tests/views/example.blade.php new file mode 100644 index 0000000..7ac2252 --- /dev/null +++ b/tests/views/example.blade.php @@ -0,0 +1,3 @@ + + Test view +