├── www ├── mu-plugins │ ├── .gitkeep │ ├── bedrock-autoloader.php │ ├── ecocide.php │ └── bedrock-site-health-tests.php ├── plugins │ └── .gitkeep ├── uploads │ └── .gitkeep ├── themes │ └── boilerplate-theme │ │ ├── assets │ │ └── .gitkeep │ │ ├── static │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── no-timber.html │ │ └── site.webmanifest │ │ ├── views │ │ ├── partial │ │ │ ├── footer.twig │ │ │ ├── header.twig │ │ │ ├── menu.twig │ │ │ └── pagination.twig │ │ ├── front-page.twig │ │ ├── 404.twig │ │ ├── page-plugin.twig │ │ ├── author.twig │ │ ├── archive.twig │ │ ├── tease-post.twig │ │ ├── tease.twig │ │ ├── layout │ │ │ ├── site-header.twig │ │ │ └── base.twig │ │ ├── page.twig │ │ ├── single-password.twig │ │ ├── search.twig │ │ ├── comment.twig │ │ ├── index.twig │ │ ├── single.twig │ │ ├── comment-form.twig │ │ └── snippet │ │ │ ├── button.twig │ │ │ └── image.twig │ │ ├── theme │ │ ├── screenshot.png │ │ ├── style.css │ │ ├── front-page.php │ │ ├── 404.php │ │ ├── search.php │ │ ├── header.php │ │ ├── author.php │ │ ├── single.php │ │ ├── functions.php │ │ ├── index.php │ │ ├── footer.php │ │ ├── page.php │ │ └── archive.php │ │ ├── includes │ │ ├── Transformer │ │ │ ├── AbstractTransformer.php │ │ │ └── Content │ │ │ │ └── ContentBlock.php │ │ ├── Template │ │ │ ├── Template.php │ │ │ └── AbstractTemplate.php │ │ ├── Traits │ │ │ └── HasContentBlocksTrait.php │ │ ├── Support │ │ │ └── Path.php │ │ └── Site.php │ │ └── phpcs.xml.dist ├── index.php ├── wp-config.php └── wp-ajax.php ├── CHANGELOG.md ├── wp-cli.example.yml ├── wp-cli.yml ├── tools └── patches │ ├── composer.patches.json │ └── timber │ └── timber │ └── 2.0 │ ├── 2733-fix-theme-location-compatibility-with-wpml.patch │ └── 2725-fix-theme-path-parsing-for-image-generation.patch ├── config ├── environments │ ├── staging.php │ └── development.php └── application.php ├── CONTRIBUTING.md ├── .gitignore ├── bin └── fix-wp-stubs.sh ├── .env.example ├── composer.json └── README.md /www/mu-plugins/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/plugins/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/uploads/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/static/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/views/partial/footer.twig: -------------------------------------------------------------------------------- 1 | {# Footer #} -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/views/partial/header.twig: -------------------------------------------------------------------------------- 1 | {# Navigation & Logo #} -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | ## 1.0.0 — TBD 6 | 7 | - Initial release 8 | -------------------------------------------------------------------------------- /wp-cli.example.yml: -------------------------------------------------------------------------------- 1 | url: https://localhost/ 2 | 3 | apache_modules: 4 | - mod_rewrite 5 | 6 | _: 7 | merge: true 8 | inherit: wp-cli.yml 9 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/views/front-page.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.twig" %} 2 | 3 | {% block content %} 4 |

{{ title }}

5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/theme/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/locomotivemtl/wordpress-boilerplate/HEAD/www/themes/boilerplate-theme/theme/screenshot.png -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/views/404.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.twig" %} 2 | 3 | {% block content %} 4 | Sorry, we couldn't find what you're looking for. 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /wp-cli.yml: -------------------------------------------------------------------------------- 1 | path: www/wordpress 2 | 3 | server: 4 | docroot: www 5 | 6 | require: 7 | - vendor/autoload.php 8 | 9 | disabled_commands: 10 | - core download 11 | - core config 12 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/views/page-plugin.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.twig" %} 2 | 3 | {% block content %} 4 |
5 | {{content}} 6 |
7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/static/no-timber.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Timber not active 5 | 6 | 7 | 8 |

Timber not activated

9 | 10 | 11 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/views/author.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.twig" %} 2 | 3 | {% block content %} 4 | {% for post in posts %} 5 | {% include ["tease-"~post.post_type~".twig", "tease.twig"] %} 6 | {% endfor %} 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/theme/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Theme Name: Boilerplate (2023) 3 | * Description: A bespoke theme for Locomotive Boilerplate. 4 | * Author: Locomotive Inc. 5 | * Author URI: https://locomotive.ca 6 | */ -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/views/archive.twig: -------------------------------------------------------------------------------- 1 | {# This file demonstrates using most of the index.twig template and modifying 2 | just a small part. See `search.twig` for an example of another approach #} 3 | 4 | {% extends "index.twig" %} 5 | 6 | {% block content %} 7 |

This is my archive

8 | {{ parent() }} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/theme/front-page.php: -------------------------------------------------------------------------------- 1 | {{ post.title }} 5 |

{{ post.excerpt({ words:5, read_more: "Keep reading" }) }}

6 | {% if post.thumbnail.src %} 7 | 8 | {% endif %} 9 | {% endblock %} -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/views/tease.twig: -------------------------------------------------------------------------------- 1 |
2 | {% block content %} 3 |

{{post.title}}

4 |

{{ post.excerpt }}

5 | {% if post.thumbnail %} 6 | 7 | {% endif %} 8 | {% endblock %} 9 |
-------------------------------------------------------------------------------- /www/themes/boilerplate-theme/views/partial/menu.twig: -------------------------------------------------------------------------------- 1 | {% if menu %} 2 | 10 | {% endif %} 11 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/views/layout/site-header.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ fn('wp_head') }} 8 | -------------------------------------------------------------------------------- /tools/patches/composer.patches.json: -------------------------------------------------------------------------------- 1 | { 2 | "patches": { 3 | "timber/timber": { 4 | "Fixes theme path parsing for image generation.": "tools/patches/timber/timber/2.0/2725-fix-theme-path-parsing-for-image-generation.patch", 5 | "Fixes nav menu location compatibility with WPML.": "tools/patches/timber/timber/2.0/2733-fix-theme-location-compatibility-with-wpml.patch" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/theme/404.php: -------------------------------------------------------------------------------- 1 | 5 |
6 |
7 |

{{post.title}}

8 |
9 | {{post.content}} 10 |
11 |
12 |
13 | 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/includes/Transformer/AbstractTransformer.php: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/includes/Template/Template.php: -------------------------------------------------------------------------------- 1 | set_context([ 22 | 'content_blocks' => $this->get_content_blocks(), 23 | ]); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /config/environments/staging.php: -------------------------------------------------------------------------------- 1 | 7 | {% for post in posts %} 8 | {% include ['tease-'~post.post_type~'.twig', 'tease.twig'] %} 9 | {% endfor %} 10 | 11 | {% include 'partial/pagination.twig' with { pagination: posts.pagination({show_all: false, mid_size: 3, end_size: 2}) } %} 12 | 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/theme/header.php: -------------------------------------------------------------------------------- 1 | 2 |
{{comment.author.name}} says
3 |
{{comment.comment_content|wpautop}}
4 | 5 |
6 | 7 | 8 | {% include "comment-form.twig" %} 9 | 10 | 11 | {% if post.comments %} 12 |

replies

13 |
14 | {% for cmt in comment.children %} 15 | {% include "comment.twig" with {comment:cmt} %} 16 | {% endfor %} 17 |
18 | {% endif %} 19 | 20 |
21 | -------------------------------------------------------------------------------- /www/mu-plugins/bedrock-autoloader.php: -------------------------------------------------------------------------------- 1 | query_vars['author'] ) ) { 20 | $author = Timber::get_user( $wp_query->query_vars['author'] ); 21 | $context['author'] = $author; 22 | $context['title'] = 'Author Archives: ' . $author->name(); 23 | } 24 | Timber::render( [ 'author.twig', 'archive.twig' ], $context ); 25 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/theme/single.php: -------------------------------------------------------------------------------- 1 | ID ) ) { 20 | Timber::render( 'single-password.twig', $context ); 21 | } else { 22 | Timber::render( [ 'single-' . $timber_post->ID . '.twig', 'single-' . $timber_post->post_type . '.twig', 'single-' . $timber_post->slug . '.twig', 'single.twig' ], $context ); 23 | } 24 | -------------------------------------------------------------------------------- /config/environments/development.php: -------------------------------------------------------------------------------- 1 | 5 | {% endblock %} 6 | 7 | 8 | 9 | {% block header %} 10 | {% include 'partial/header.twig' with { is_fixed: true } %} 11 | {% endblock %} 12 | 13 |
14 | {% block hero %}{% endblock %} 15 | 16 |
17 | {% block content %} 18 | {{ _e( 'Sorry, no content', 'app/theme') }} 19 | {% endblock %} 20 |
21 |
22 | 23 | {% block footer %} 24 | {% include 'partial/footer.twig' %} 25 | {% endblock %} 26 | 27 | {{ fn('wp_footer') }} 28 | 29 | 30 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/views/index.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.twig" %} 2 | 3 | {% block content %} 4 |

{{ foo }}

5 |

{{ qux }}

6 | {% for post in posts %} 7 | {% include ['tease-'~post.post_type~'.twig', 'tease.twig'] %} 8 | {% endfor %} 9 | 10 |
11 |
12 |

Amet consectetur adipisicing

13 |

Lorem ipsum dolor sit amet consectetur adipisicing elit. Nisi reiciendis alias aspernatur mollitia commodi. Soluta, sint corrupti quaerat cum aspernatur nostrum tempore ad, eveniet animi voluptatibus fugiat qui laboriosam debitis.

14 |
15 |
16 | 17 | {% include 'partial/pagination.twig' with { pagination: posts.pagination({show_all: false, mid_size: 3, end_size: 2}) } %} 18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/theme/functions.php: -------------------------------------------------------------------------------- 1 | post_name . '.twig', 'page.twig' ], $context ); 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # WordPress 2 | # ---------------- 3 | 4 | /www/* 5 | !/www/index.php 6 | !/www/wp-config.php 7 | 8 | !/www/mu-plugins 9 | /www/mu-plugins/*/ 10 | 11 | !/www/plugins 12 | /www/plugins/*/ 13 | 14 | !/www/themes 15 | 16 | !/www/uploads 17 | /www/uploads/* 18 | !/www/uploads/.gitkeep 19 | 20 | # Application 21 | # ---------------- 22 | 23 | !/www/wp-ajax.php 24 | #~ !/www/mu-plugins/foo 25 | #~ !/www/themes/qux 26 | 27 | /www/mu-plugins/wp-env-*.php 28 | !/www/mu-plugins/wp-env-production.php 29 | 30 | !/www/apple-touch-icon.png 31 | !/www/favicon.* 32 | 33 | # WP-CLI 34 | # ---------------- 35 | 36 | .wp-cli 37 | wp-cli.local.yml 38 | 39 | # Dotenv 40 | # ---------------- 41 | 42 | .env 43 | .env.* 44 | !.env.example 45 | 46 | # Development 47 | # ---------------- 48 | 49 | phpcs.xml 50 | phpstan.neon 51 | phpunit.xml 52 | psalm.xml 53 | 54 | # Logs 55 | # ---------------- 56 | 57 | *.log 58 | 59 | # Package Managers 60 | # ---------------- 61 | 62 | /node_modules 63 | /vendor 64 | auth.json 65 | 66 | # Project Managers 67 | # ---------------- 68 | 69 | /.idea 70 | /.nova 71 | /.vagrant 72 | -------------------------------------------------------------------------------- /bin/fix-wp-stubs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Exclude pluggable functions in WordPress stubs. 4 | # 5 | # WordPress uses conditional function and class definition for override purposes. 6 | # 7 | # See: https://github.com/szepeviktor/phpstan-wordpress/tree/v1.1.2#dirty-corner-faq 8 | # See: https://codex.wordpress.org/Pluggable_Functions 9 | # 10 | 11 | if [[ "$OSTYPE" == "darwin"* ]]; then 12 | SEDOPTION="-i ''" 13 | else 14 | SEDOPTION='-i' 15 | fi; 16 | 17 | # 18 | # Exclude pluggable functions overridden by roots/wp-password-bcrypt 19 | # 20 | # Arguments: 21 | # 1. ...files - The PHP files to patch. 22 | # 23 | fix_for_wp_password_bcrypt() { 24 | for file in "$@"; do 25 | sed -e 's/function wp_check_password/function __wp_check_password/' \ 26 | -e 's/function wp_hash_password/function __wp_hash_password/' \ 27 | -e 's/function wp_set_password/function __wp_set_password/' \ 28 | $SEDOPTION $file 29 | done 30 | } 31 | 32 | FILE=vendor/php-stubs/wordpress-stubs/wordpress-stubs.php 33 | if [[ -f "$FILE" ]]; then 34 | echo "- Excluding pluggable functions overridden by roots/wp-password-bcrypt" 35 | fix_for_wp_password_bcrypt $FILE 36 | fi 37 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/includes/Transformer/Content/ContentBlock.php: -------------------------------------------------------------------------------- 1 | $layout, 23 | 'template' => $this->getTemplatePath($layout), 24 | 'data' => $data, 25 | ]; 26 | } 27 | 28 | /** 29 | * @param string|null $layout 30 | * @return string 31 | */ 32 | protected function getTemplatePath(?string $layout = null): string 33 | { 34 | $path = get_template_directory() . '/views/blocks/block'; 35 | 36 | if (!empty($layout)) { 37 | $path .= '-' . $layout; 38 | } 39 | 40 | return $path . '.twig'; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Database 2 | # ---------------- 3 | 4 | DB_NAME='database_name' 5 | DB_USER='database_user' 6 | DB_PASSWORD='database_password' 7 | 8 | # Optionally, you can use a data source name (DSN) 9 | # When using a DSN, you can remove the DB_NAME, DB_USER, DB_PASSWORD, and DB_HOST variables 10 | #~ DATABASE_URL='mysql://database_user:database_password@database_host:database_port/database_name' 11 | 12 | # Optional database variables 13 | #~ DB_HOST='localhost' 14 | #~ DB_PREFIX='wp_' 15 | 16 | # Environment 17 | # ---------------- 18 | 19 | WP_DEVELOPMENT_MODE='' 20 | WP_ENVIRONMENT_TYPE='local' 21 | WP_HOME='https://localhost' 22 | WP_SITEURL="${WP_HOME}/wordpress" 23 | 24 | # Specify optional debug.log path 25 | #~ WP_DEBUG_LOG='/path/to/debug.log' 26 | 27 | # Licensing 28 | # ---------------- 29 | # For Composer installation 30 | 31 | ACF_PRO_KEY='' 32 | GRAVITY_FORMS_KEY='' 33 | POLYLANG_PRO_KEY='' 34 | POLYLANG_PRO_URL="${WP_HOME}" 35 | 36 | # Security 37 | # ---------------- 38 | # Generate your keys via 39 | # - https://roots.io/salts.html 40 | # - wp dotenv salts regenerate 41 | 42 | AUTH_KEY='' 43 | SECURE_AUTH_KEY='' 44 | LOGGED_IN_KEY='' 45 | NONCE_KEY='' 46 | AUTH_SALT='' 47 | SECURE_AUTH_SALT='' 48 | LOGGED_IN_SALT='' 49 | NONCE_SALT='' 50 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/views/single.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout/base.twig" %} 2 | 3 | {% block content %} 4 |
5 |
6 | 7 |
8 |

{{ post.title }}

9 | {{ _e('edit') }} 10 |

11 | By {{ post.author.name }} 12 |

13 |
14 | {{post.content}} 15 |
16 |
17 | 18 | 19 |
20 | 21 |
22 | {% if post.comments %} 23 |

comments

24 | {% for cmt in post.comments %} 25 | {% include "comment.twig" with {comment:cmt} %} 26 | {% endfor %} 27 | {% endif %} 28 |
29 | 30 | {% if post.comment_status == "closed" %} 31 |

comments for this post are closed

32 | {% else %} 33 | 34 | {% include "comment-form.twig" %} 35 | {% endif %} 36 |
37 |
38 |
39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/views/comment-form.twig: -------------------------------------------------------------------------------- 1 |
2 |

Add comment

3 |
4 | {% if user %} 5 | 6 | 7 | 8 | {% else %} 9 | 12 | 15 | 18 | {% endif %} 19 | 22 | 23 | 24 | 25 | 26 |

Your comment will be revised by the site if needed.

27 |
28 |
29 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/views/snippet/button.twig: -------------------------------------------------------------------------------- 1 | {% set _tag = tag | default('button') %} 2 | {% set _href = href | default(null) %} 3 | {% set _classes = classes | default(null) %} 4 | {% set _modifiers = modifiers | default(null) %} 5 | {% set _attr = attr | default(null) %} 6 | {% set _label = label | default(null) %} 7 | {% set _external = external | default(false) %} 8 | {% set _icon = icon | default(null) %} 9 | 10 | {% if _tag == 'a' and _href == null %} 11 | {% set _tag = 'span' %} 12 | {% endif %} 13 | 14 | {% if _href != null %} 15 | {% set _tag = 'a' %} 16 | {% endif %} 17 | 18 | <{{ _tag }} 19 | class="c-button {{ _modifiers }} {{ _classes }}" 20 | {% if _href %}href="{{ _href }}"{% endif %} 21 | {% if _external %}target="_blank" rel="noopener noreferrer"{% endif %} 22 | {% if _attr %}{{ _attr|raw }}{% endif %} 23 | > 24 | {% block inner %} 25 | 26 | {{ _label }} 27 | 28 | 29 | {% if _icon %} 30 | 31 | 32 | 35 | 36 | 37 | {% endif %} 38 | {% endblock %} 39 | 40 | -------------------------------------------------------------------------------- /www/mu-plugins/ecocide.php: -------------------------------------------------------------------------------- 1 | get('disable-attachment-template')->boot(); 32 | $ecocide->get('disable-author-template')->boot(); 33 | $ecocide->get('disable-comments')->boot(); 34 | $ecocide->get('disable-customizer')->boot(); 35 | $ecocide->get('disable-emoji')->boot(); 36 | $ecocide->get('disable-post')->boot(); 37 | $ecocide->get('disable-post-category')->boot(); 38 | $ecocide->get('disable-post-format')->boot(); 39 | $ecocide->get('disable-post-tag')->boot(); 40 | $ecocide->get('disable-search')->boot(); 41 | $ecocide->get('disable-xml-rpc')->boot(); 42 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/theme/archive.php: -------------------------------------------------------------------------------- 1 | content_blocks)) { 21 | $blocks = $this->get_fields()['content_blocks'] ?? []; 22 | if (!empty($blocks) && have_rows('content_blocks')) { 23 | foreach ($blocks as $block_data) { 24 | the_row(); 25 | $block = $this->transform_block($block_data); 26 | if (!empty($block)) { 27 | $this->content_blocks[] = $block; 28 | } 29 | } 30 | } 31 | } 32 | return $this->content_blocks; 33 | } 34 | 35 | /** 36 | * @param array $block 37 | * @return array 38 | */ 39 | public function transform_block(array $block = []): array 40 | { 41 | $transformer = $this->resolve_block_transformer($block); 42 | return $this->transform($block, $transformer); 43 | } 44 | 45 | /** 46 | * @param array $block 47 | * @return string 48 | */ 49 | public function resolve_block_transformer(array $block): string 50 | { 51 | $layout = !empty($block['acf_fc_layout']) ? $block['acf_fc_layout'] : null; 52 | $namespace = 'App\Theme\Transformer\Content\\'; 53 | 54 | if (!empty($layout)) { 55 | $layout = str_replace('_', '', ucwords($layout, '_')); 56 | $class = $namespace . $layout; 57 | 58 | if (class_exists($class)) { 59 | return $class; 60 | } 61 | } 62 | 63 | return $namespace . 'ContentBlock'; 64 | } 65 | 66 | /** 67 | * @param array $data 68 | * @param string $transformer 69 | * @return ?array 70 | */ 71 | abstract public function transform(array $data, string $transformer): ?array; 72 | } 73 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/includes/Template/AbstractTemplate.php: -------------------------------------------------------------------------------- 1 | post = Timber::get_post(); 28 | 29 | $context['post'] = $this->post; 30 | $this->set_context($context); 31 | } 32 | 33 | /** 34 | * Set Context Data 35 | * 36 | * @param array $data Context data. 37 | * @return $this 38 | */ 39 | public function set_context(array $data = []) 40 | { 41 | $context = $this->get_context(); 42 | $this->context = array_merge($context, $data); 43 | return $this; 44 | } 45 | 46 | /** 47 | * Get Context Data 48 | * 49 | * @return array 50 | */ 51 | public function get_context() 52 | { 53 | if (!isset($this->context)) { 54 | $this->context = []; 55 | } 56 | return $this->context; 57 | } 58 | 59 | /** 60 | * Get ACF Fields 61 | * 62 | * @return array 63 | */ 64 | public function get_fields() 65 | { 66 | if (!isset($this->fields)) { 67 | $this->fields = []; 68 | 69 | if (!empty($this->post->ID)) { 70 | $this->fields = get_fields($this->post->ID); 71 | } 72 | } 73 | return $this->fields; 74 | } 75 | 76 | /** 77 | * Get Post 78 | * 79 | * @return Timber\Post|boolean 80 | */ 81 | public function get_post() 82 | { 83 | if (!isset($this->post)) { 84 | return false; 85 | } 86 | return $this->post; 87 | } 88 | 89 | /** 90 | * Transform data. 91 | * 92 | * @param array $data 93 | * @param string $transformer 94 | * @return ?array 95 | */ 96 | public function transform(array $data = [], string $transformer): ?array 97 | { 98 | $transformer = (new $transformer); 99 | return $transformer($data); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | WordPress Boilerplate Coding Standards 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | . 18 | 19 | 20 | node_modules 21 | tests 22 | vendor 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | /tests/* 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/views/partial/pagination.twig: -------------------------------------------------------------------------------- 1 | {% if posts.pagination.pages is not empty %} 2 | 64 | {% endif %} 65 | -------------------------------------------------------------------------------- /www/mu-plugins/bedrock-site-health-tests.php: -------------------------------------------------------------------------------- 1 | $result An associative array of test result data. 27 | * @return array 28 | */ 29 | function filter_site_status_test_result( array $result ) : array { 30 | if ( 31 | ! isset( $result['test'] ) || 32 | $result['test'] !== 'background_updates' 33 | ) { 34 | return $result; 35 | } 36 | 37 | return override_test_background_updates( $result ); 38 | } 39 | 40 | /** 41 | * Filters the REST API response for the Background updates test 42 | * to override the result. 43 | * 44 | * @param WP_HTTP_Response $result The REST API response. 45 | * @return WP_HTTP_Response 46 | */ 47 | function filter_rest_post_dispatch( WP_HTTP_Response $result ) : WP_HTTP_Response { 48 | $data = $result->get_data(); 49 | if ( 50 | ! is_array( $data ) || 51 | ! isset( $data['test'] ) || 52 | $data['test'] !== 'background_updates' 53 | ) { 54 | return $result; 55 | } 56 | 57 | /** @var array $data */ 58 | 59 | $result->set_data( override_test_background_updates( $data ) ); 60 | return $result; 61 | } 62 | 63 | /** 64 | * Overrides the result of the finished Background updates test 65 | * to avoid conflict with the way Bedrock is structured 66 | * (managed by Composer and version control system). 67 | * 68 | * @param array $result An associative array of test result data. 69 | * @return array 70 | */ 71 | function override_test_background_updates( array $result ) : array { 72 | $description = sprintf( 73 | '

%s

', 74 | __( 'This site is under version control. Updates are managed by Composer.' ) 75 | ); 76 | 77 | if ( 78 | isset( $result['description'] ) && 79 | is_string( $result['description'] ) 80 | ) { 81 | $description .= sprintf( 82 | '
%s
', 83 | $result['description'] 84 | ); 85 | } 86 | 87 | return array_replace( $result, [ 88 | 'label' => __( 'Background updates are disabled by Bedrock' ), 89 | 'status' => 'good', 90 | 'description' => $description, 91 | ] ); 92 | } 93 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/views/snippet/image.twig: -------------------------------------------------------------------------------- 1 | {# 2 | Image snippet 3 | 4 | The `img` parameter was made to receive formatted data from a CMS. 5 | The use case would be to output an image without overriding is dimensions or other properties. 6 | 7 | It needs to be an object with the following keys : 8 | -src: String, 9 | -width: Int, 10 | -height: Int, 11 | -alt?: String, 12 | -caption?: String 13 | 14 | ```twig 15 | {% include 'image' with 16 | img: project.featured_image 17 | %} 18 | ``` 19 | #} 20 | 21 | {# Defaults #} 22 | {% set _width = img.width | default(1) %} 23 | {% set _height = img.height | default(1) %} 24 | {% set _src = img.src | default(null) %} 25 | {% set _alt = img.alt | default(null) %} 26 | {% set _caption = img.caption | default(null) %} 27 | 28 | {# Override properties #} 29 | {% set _width = width | default(_width) %} 30 | {% set _height = height | default(_height) %} 31 | {% set _src = src | default(_src) %} 32 | {% set _alt = alt | default(_alt) %} 33 | {% set _caption = caption | default(_caption) %} 34 | 35 | {# Misc. #} 36 | {% set _is_figure = is_figure | default(false) %} 37 | {% set _is_lazy_load = is_lazy_load | default(null) %} 38 | {% set _has_border_radius = has_border_radius | default(null) %} 39 | {% set _tag = _is_figure ? 'figure' : 'div' %} 40 | 41 | {# Classes & modifiers #} 42 | {% set _classes = classes | default(null) %} 43 | {% set _modifiers = modifiers | default(null) %} 44 | 45 | {% if _is_lazy_load %} 46 | {% set _modifiers = _modifiers ~ ' -lazy-load' %} 47 | {% endif %} 48 | 49 | {% if _has_border_radius %} 50 | {% set _modifiers = _modifiers ~ ' -border-radius' %} 51 | {% endif %} 52 | 53 | {% if _classes != null %} 54 | {% set _classes = ' ' ~ _classes %} 55 | {% endif %} 56 | 57 | {% if _modifiers != null %} 58 | {% set _classes = ' ' ~ _modifiers ~ ' ' ~ _classes %} 59 | {% endif %} 60 | 61 | {# ---------------------------------------- #} 62 | 63 | <{{_tag}} class="c-image{{ _classes }}"> 64 |
65 | {{ _alt }} 79 |
80 | 81 | {% if _caption %} 82 | {% if _is_figure %} 83 |
{{ _caption }}
84 | {% else %} 85 |
{{ _caption }}
86 | {% endif %} 87 | {% endif %} 88 | 89 | -------------------------------------------------------------------------------- /www/wp-ajax.php: -------------------------------------------------------------------------------- 1 | 0 && 103 | '..' !== $canonicalParts[ count( $canonicalParts ) - 1 ] 104 | ) { 105 | array_pop($canonicalParts); 106 | 107 | continue; 108 | } 109 | 110 | // Only add `..` prefixes for relative paths 111 | if ( '..' !== $part || '' === $root ) { 112 | $canonicalParts[] = $part; 113 | } 114 | } 115 | 116 | return $canonicalParts; 117 | } 118 | 119 | /** 120 | * Splits a part into its root directory or domain name and the remainder. 121 | * 122 | * If the path has no root directory or domain name, an empty root directory will be 123 | * returned. 124 | * 125 | * @param string $path The canonical path or URL to split. 126 | * @return string[] An array with: 127 | * 1. If a URL, the scheme, authority, and host, otherwise the root directory. 128 | * 2. The remaining relative path. 129 | * 3. If a URL, the query and fragment. 130 | */ 131 | private static function split( string $path ) : array 132 | { 133 | if ( '' === $path ) { 134 | return [ '', '', '' ]; 135 | } 136 | 137 | if ( false !== ( $schemeSeparatorPosition = strpos( $path, '://' ) ) ) { 138 | // Check if it's a URL 139 | if ( in_array( parse_url( $path, PHP_URL_SCHEME ), wp_allowed_protocols(), true ) ) { 140 | $url = $path; 141 | $path = parse_url( $path, PHP_URL_PATH ); 142 | [ $root, $extra ] = array_pad( explode( $path, $url ), 2, '' ); 143 | } else { 144 | // Remember scheme as part of the root, if any 145 | $root = substr( $path, 0, $schemeSeparatorPosition + 3 ); 146 | $path = substr( $path, $schemeSeparatorPosition + 3 ); 147 | $extra = ''; 148 | } 149 | } else { 150 | $root = ''; 151 | $extra = ''; 152 | } 153 | 154 | $length = strlen( $path ); 155 | 156 | // Remove and remember root directory 157 | if ( str_starts_with( $path, '/' ) ) { 158 | $root .= '/'; 159 | $path = $length > 1 ? substr( $path, 1 ) : ''; 160 | } elseif ( $length > 1 && ctype_alpha( $path[0] ) && ':' === $path[1] ) { 161 | if ( 2 === $length ) { 162 | // Windows special case: `C:` 163 | $root .= $path.'/'; 164 | $path = ''; 165 | } elseif ( '/' === $path[2] ) { 166 | // Windows normal case: `C:/`.. 167 | $root .= substr($path, 0, 3); 168 | $path = $length > 3 ? substr($path, 3) : ''; 169 | } 170 | } 171 | 172 | return [ $root, $path, $extra ]; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /tools/patches/timber/timber/2.0/2733-fix-theme-location-compatibility-with-wpml.patch: -------------------------------------------------------------------------------- 1 | # 2 | # Fixes menu location compatibility with WPML. 3 | # 4 | # Adds a menu location retrieval methods to the `Timber` class 5 | # and adds a hook on the `theme_mod_nav_menu_locations` filter 6 | # to translate menu IDs. 7 | # 8 | # This patch is a subset of the changeset of timber/timber#2733. 9 | # 10 | # See: 11 | # - https://github.com/timber/timber/pull/2733 12 | # 13 | diff --git a/src/Factory/MenuFactory.php b/src/Factory/MenuFactory.php 14 | index 34cbd8ef..efa35c4b 100644 15 | --- a/src/Factory/MenuFactory.php 16 | +++ b/src/Factory/MenuFactory.php 17 | @@ -5,6 +5,7 @@ namespace Timber\Factory; 18 | use InvalidArgumentException; 19 | use Timber\CoreInterface; 20 | use Timber\Menu; 21 | +use Timber\Timber; 22 | use WP_Term; 23 | 24 | /** 25 | @@ -87,7 +88,7 @@ class MenuFactory 26 | */ 27 | public function from_location(string $location, array $args = []): ?Menu 28 | { 29 | - $locations = \get_nav_menu_locations(); 30 | + $locations = Timber::get_menu_locations(); 31 | if (!isset($locations[$location])) { 32 | return null; 33 | } 34 | @@ -212,7 +213,7 @@ class MenuFactory 35 | */ 36 | $classmap = \apply_filters('timber/menu/classmap', []); 37 | 38 | - $location = $this->get_menu_location($term); 39 | + $location = Timber::get_menu_location($term); 40 | 41 | $class = $classmap[$location] ?? null; 42 | 43 | @@ -251,18 +252,6 @@ class MenuFactory 44 | return $class; 45 | } 46 | 47 | - /** 48 | - * Get the menu location 49 | - * 50 | - * @param WP_Term $term 51 | - * @return string|null 52 | - */ 53 | - protected function get_menu_location(WP_Term $term): ?string 54 | - { 55 | - $locations = \array_flip(\array_filter(\get_nav_menu_locations(), fn ($location) => \is_string($location) || \is_int($location))); 56 | - return $locations[$term->term_id] ?? null; 57 | - } 58 | - 59 | /** 60 | * Build menu 61 | * 62 | diff --git a/src/Integration/WpmlIntegration.php b/src/Integration/WpmlIntegration.php 63 | index 6b19d45c..142fc4fc 100644 64 | --- a/src/Integration/WpmlIntegration.php 65 | +++ b/src/Integration/WpmlIntegration.php 66 | @@ -14,10 +14,10 @@ class WpmlIntegration implements IntegrationInterface 67 | 68 | public function init(): void 69 | { 70 | + \add_filter('theme_mod_nav_menu_locations', [$this, 'theme_mod_nav_menu_locations'], 10, 1); 71 | \add_filter('timber/url_helper/file_system_to_url', [$this, 'file_system_to_url'], 10, 1); 72 | \add_filter('timber/url_helper/get_content_subdir/home_url', [$this, 'file_system_to_url'], 10, 1); 73 | \add_filter('timber/url_helper/url_to_file_system/path', [$this, 'file_system_to_url'], 10, 1); 74 | - \add_filter('timber/menu/id_from_location', [$this, 'menu_object_id_filter'], 10, 1); 75 | \add_filter('timber/menu/item_objects', [$this, 'menu_item_objects_filter'], 10, 1); 76 | \add_filter('timber/image_helper/_get_file_url/home_url', [$this, 'file_system_to_url'], 10, 1); 77 | } 78 | @@ -30,11 +30,6 @@ class WpmlIntegration implements IntegrationInterface 79 | return $url; 80 | } 81 | 82 | - public function menu_object_id_filter($id) 83 | - { 84 | - return \wpml_object_id_filter($id, 'nav_menu'); 85 | - } 86 | - 87 | public function menu_item_objects_filter(array $items) 88 | { 89 | return \array_map( 90 | @@ -42,4 +37,16 @@ class WpmlIntegration implements IntegrationInterface 91 | $items 92 | ); 93 | } 94 | + 95 | + public function theme_mod_nav_menu_locations($locations) 96 | + { 97 | + if (!\is_array($locations)) { 98 | + return $locations; 99 | + } 100 | + 101 | + return \array_map( 102 | + fn ($id) => \wpml_object_id_filter($id, 'nav_menu'), 103 | + $locations 104 | + ); 105 | + } 106 | } 107 | diff --git a/src/Menu.php b/src/Menu.php 108 | index b23d4170..eb8cd82a 100644 109 | --- a/src/Menu.php 110 | +++ b/src/Menu.php 111 | @@ -293,10 +293,7 @@ class Menu extends CoreEntity 112 | } 113 | 114 | // Set theme location if available 115 | - $locations = \array_flip(\array_filter(\get_nav_menu_locations(), fn ($location) => \is_string($location) || \is_int($location))); 116 | - 117 | - $this->theme_location = $locations[$term->term_id] ?? null; 118 | - 119 | + $this->theme_location = Timber::get_menu_location($term); 120 | if ($this->theme_location) { 121 | $this->args->theme_location = $this->theme_location; 122 | } 123 | diff --git a/src/Timber.php b/src/Timber.php 124 | index 69e0a568..593d9fb7 100644 125 | --- a/src/Timber.php 126 | +++ b/src/Timber.php 127 | @@ -1054,6 +1054,39 @@ class Timber 128 | return $menu; 129 | } 130 | 131 | + /** 132 | + * Get the navigation menu location assigned to the given menu. 133 | + * 134 | + * @param WP_Term|int $term The menu to find; either a WP_Term object or a Term ID. 135 | + * @return string|null 136 | + */ 137 | + public static function get_menu_location($term): ?string 138 | + { 139 | + if ($term instanceof WP_Term) { 140 | + $term_id = $term->term_id; 141 | + } elseif (\is_int($term)) { 142 | + $term_id = $term; 143 | + } else { 144 | + return null; 145 | + } 146 | + 147 | + $locations = \array_flip(static::get_menu_locations()); 148 | + return $locations[$term->term_id] ?? null; 149 | + } 150 | + 151 | + /** 152 | + * Get the navigation menu locations with assigned menus. 153 | + * 154 | + * @return array 155 | + */ 156 | + public static function get_menu_locations(): array 157 | + { 158 | + return \array_filter( 159 | + \get_nav_menu_locations(), 160 | + fn ($location) => \is_string($location) || \is_int($location) 161 | + ); 162 | + } 163 | + 164 | 165 | /* Comment Retrieval 166 | ================================ */ 167 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "project", 3 | "name": "locomotivemtl/wordpress-boilerplate", 4 | "description": "A modern WordPress project boilerplate.", 5 | "license": "MIT", 6 | "keywords": [ 7 | "boilerplate", 8 | "composer", 9 | "locomotive", 10 | "wordpress", 11 | "wp", 12 | "wp-config" 13 | ], 14 | "support": { 15 | "source": "https://github.com/locomotivemtl/wordpress-boilerplate", 16 | "issues": "https://github.com/locomotivemtl/wordpress-boilerplate/issues" 17 | }, 18 | "authors": [ 19 | { 20 | "name": "Locomotive", 21 | "homepage": "https://locomotive.ca" 22 | }, 23 | { 24 | "name": "Chauncey McAskill", 25 | "email": "chauncey@mcaskill.ca", 26 | "homepage": "https://mcaskill.ca" 27 | } 28 | ], 29 | "require": { 30 | "php": ">=7.4", 31 | "composer/installers": "^1.0 || ^2.0", 32 | "cweagans/composer-patches": "^1.7", 33 | "junaidbhura/advanced-custom-fields-pro": "*", 34 | "junaidbhura/composer-wp-pro-plugins": "^1.4", 35 | "locomotivemtl/wp-lib-cms": "^1.0@dev", 36 | "locomotivemtl/wp-lib-theme": "^1.0@dev", 37 | "oscarotero/env": "^2.1", 38 | "roots/bedrock-autoloader": "^1.0", 39 | "roots/bedrock-disallow-indexing": "^2.0", 40 | "roots/wordpress": "^6.3.0", 41 | "roots/wp-config": "^1.0.0", 42 | "roots/wp-password-bcrypt": "^1.1.0", 43 | "twig/html-extra": "^3.4", 44 | "vlucas/phpdotenv": "^5.5", 45 | "wikimedia/composer-merge-plugin": "^2.0", 46 | "wpackagist-plugin/aryo-activity-log": "^2.8", 47 | "wpackagist-plugin/classic-editor": "^1.6", 48 | "wpackagist-plugin/redirection": "^5.3" 49 | }, 50 | "require-dev": { 51 | "roave/security-advisories": "dev-latest", 52 | "wp-jazz/coding-standards": "^1" 53 | }, 54 | "autoload": { 55 | "psr-4": { 56 | "App\\Theme\\": "www/themes/boilerplate-theme/includes/" 57 | } 58 | }, 59 | "minimum-stability": "dev", 60 | "prefer-stable": true, 61 | "config": { 62 | "allow-plugins": { 63 | "composer/installers": true, 64 | "cweagans/composer-patches": true, 65 | "dealerdirect/phpcodesniffer-composer-installer": true, 66 | "junaidbhura/composer-wp-pro-plugins": true, 67 | "roots/wordpress-core-installer": true, 68 | "wecodemore/wp-package-assets-publisher": true, 69 | "wikimedia/composer-merge-plugin": true 70 | }, 71 | "optimize-autoloader": true, 72 | "preferred-install": "dist", 73 | "sort-packages": true 74 | }, 75 | "extra": { 76 | "composer-exit-on-patch-failure": true, 77 | "branch-alias": { 78 | "dev-main": "1.x-dev" 79 | }, 80 | "installer-paths": { 81 | "www/mu-plugins/{$name}/": [ 82 | "type:wordpress-muplugin", 83 | "wpackagist-plugin/classic-editor" 84 | ], 85 | "www/plugins/{$name}/": [ 86 | "type:wordpress-plugin" 87 | ], 88 | "www/themes/{$name}/": [ 89 | "type:wordpress-theme" 90 | ], 91 | "www/{$name}/": [ 92 | "type:wordpress-dropin" 93 | ] 94 | }, 95 | "merge-plugin": { 96 | "include": [ 97 | "www/mu-plugins/app/composer.json" 98 | ] 99 | }, 100 | "patches-file": "tools/patches/composer.patches.json", 101 | "wordpress-install-dir": "www/wordpress" 102 | }, 103 | "scripts": { 104 | "post-root-package-install": [ 105 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 106 | ], 107 | "post-install-cmd": [ 108 | "@fix:wp-stubs" 109 | ], 110 | "post-update-cmd": [ 111 | "@fix:wp-stubs" 112 | ], 113 | "fix:wp-stubs": [ 114 | "bin/fix-wp-stubs.sh" 115 | ], 116 | "lint": [ 117 | "@lint:phpcs", 118 | "@lint:phpstan", 119 | "@lint:psalm" 120 | ], 121 | "lint:phpcs": [ 122 | "phpcs -ps --colors" 123 | ], 124 | "lint:phpstan": [ 125 | "phpstan analyze" 126 | ], 127 | "lint:psalm": [ 128 | "psalm" 129 | ] 130 | }, 131 | "repositories": [ 132 | { 133 | "type": "vcs", 134 | "url": "https://github.com/locomotivemtl/wp-lib-cms", 135 | "only": [ 136 | "locomotivemtl/wp-lib-cms" 137 | ] 138 | }, 139 | { 140 | "type": "vcs", 141 | "url": "https://github.com/locomotivemtl/wp-lib-theme", 142 | "only": [ 143 | "locomotivemtl/wp-lib-theme" 144 | ] 145 | }, 146 | { 147 | "type": "composer", 148 | "url": "https://wpackagist.org", 149 | "only": [ 150 | "wpackagist-plugin/*", 151 | "wpackagist-theme/*" 152 | ] 153 | }, 154 | { 155 | "type": "vcs", 156 | "url": "https://github.com/junaidbhura/composer-wp-pro-plugins", 157 | "only": [ 158 | "junaidbhura/composer-wp-pro-plugins" 159 | ] 160 | }, 161 | { 162 | "type": "package", 163 | "package": { 164 | "name": "junaidbhura/advanced-custom-fields-pro", 165 | "version": "6.2.0", 166 | "type": "wordpress-muplugin", 167 | "dist": { 168 | "type": "zip", 169 | "url": "https://www.advancedcustomfields.com/" 170 | }, 171 | "require": { 172 | "junaidbhura/composer-wp-pro-plugins": "*" 173 | }, 174 | "replace": { 175 | "wpackagist-plugin/advanced-custom-fields": "self.version" 176 | }, 177 | "provide": { 178 | "wpackagist-plugin/advanced-custom-fields-implementation": "self.version" 179 | } 180 | } 181 | }, 182 | { 183 | "type": "package", 184 | "package": { 185 | "name": "junaidbhura/gravityforms", 186 | "version": "2.7.13", 187 | "type": "wordpress-muplugin", 188 | "dist": { 189 | "type": "zip", 190 | "url": "https://www.gravityforms.com/" 191 | }, 192 | "require": { 193 | "junaidbhura/composer-wp-pro-plugins": "*" 194 | }, 195 | "provide": { 196 | "wpackagist-plugin/gravityforms-implementation": "self.version" 197 | } 198 | } 199 | }, 200 | { 201 | "type": "package", 202 | "package": { 203 | "name": "junaidbhura/polylang-pro", 204 | "version": "3.4.5", 205 | "type": "wordpress-muplugin", 206 | "dist": { 207 | "type": "zip", 208 | "url": "https://www.polylang.pro/" 209 | }, 210 | "require": { 211 | "junaidbhura/composer-wp-pro-plugins": "*" 212 | }, 213 | "replace": { 214 | "wpackagist-plugin/polylang": "self.version" 215 | }, 216 | "provide": { 217 | "wpackagist-plugin/polylang-implementation": "self.version" 218 | } 219 | } 220 | } 221 | ] 222 | } 223 | -------------------------------------------------------------------------------- /config/application.php: -------------------------------------------------------------------------------- 1 | load(); 34 | $dotenv->required( [ 35 | 'WP_HOME', 36 | 'WP_SITEURL', 37 | ] ); 38 | 39 | if ( ! env( 'DATABASE_URL' ) ) { 40 | $dotenv->required( [ 41 | 'DB_NAME', 42 | 'DB_USER', 43 | 'DB_PASSWORD', 44 | ] ); 45 | } 46 | } 47 | 48 | /** 49 | * The global development mode constant for WordPress and the project. 50 | * 51 | * @var string|null 52 | */ 53 | $wp_dev_mode = env( 'WP_DEVELOPMENT_MODE' ); 54 | 55 | /** 56 | * The `WP_DEVELOPMENT_MODE` constant is officially supported by WordPress. 57 | */ 58 | define( 'WP_DEVELOPMENT_MODE', ( $wp_dev_mode ?? '' ) ); 59 | 60 | /** 61 | * The global environment constant for WordPress and the project. 62 | * 63 | * @var string|null 64 | */ 65 | $wp_env_type = env( 'WP_ENVIRONMENT_TYPE' ); 66 | 67 | /** 68 | * The `WP_ENVIRONMENT_TYPE` constant is officially supported by WordPress. 69 | */ 70 | define( 'WP_ENVIRONMENT_TYPE', ( $wp_env_type ?? 'production' ) ); 71 | 72 | /** 73 | * The `WP_ENV` constant is required by certain plugins from Roots for Bedrock. 74 | */ 75 | define( 'WP_ENV', WP_ENVIRONMENT_TYPE ); 76 | 77 | /** 78 | * URLs 79 | */ 80 | Config::define( 'WP_HOME', env( 'WP_HOME' ) ); 81 | Config::define( 'WP_SITEURL', env( 'WP_SITEURL' ) ); 82 | Config::define( 'WP_CONTENT_URL', Config::get( 'WP_HOME' ) ); 83 | 84 | /** 85 | * Database Settings 86 | */ 87 | if ( env( 'DB_SSL' ) ) { 88 | Config::define( 'MYSQL_CLIENT_FLAGS', MYSQLI_CLIENT_SSL ); 89 | } 90 | 91 | Config::define( 'DB_NAME', env( 'DB_NAME' ) ); 92 | Config::define( 'DB_USER', env( 'DB_USER' ) ); 93 | Config::define( 'DB_PASSWORD', env( 'DB_PASSWORD' ) ); 94 | Config::define( 'DB_HOST', ( env( 'DB_HOST' ) ?? 'localhost' ) ); 95 | Config::define( 'DB_CHARSET', ( env( 'DB_CHARSET' ) ?? 'utf8mb4' ) ); 96 | Config::define( 'DB_COLLATE', ( env( 'DB_COLLATE' ) ?? '' ) ); 97 | Config::define( 'DB_PREFIX', ( env( 'DB_PREFIX' ) ?? 'wp_' ) ); 98 | 99 | /** 100 | * The database table prefix. Assigned to a global variable 101 | * in {@see /wordpress/wp-settings.php}. 102 | * 103 | * @var string 104 | */ 105 | $table_prefix = Config::get( 'DB_PREFIX' ); 106 | 107 | /** 108 | * The Data Source Name (DSN) for connecting to a database. 109 | * 110 | * @var string|null 111 | */ 112 | $dsn = env( 'DATABASE_URL' ); 113 | 114 | if ( $dsn ) { 115 | /** @psalm-var array{host:string, port: ?int, user: string, pass: ?string, path: string} */ 116 | $dsn = parse_url( $dsn ); 117 | 118 | Config::define( 'DB_NAME', substr( $dsn['path'], 1 ) ); 119 | Config::define( 'DB_USER', $dsn['user'] ); 120 | Config::define( 'DB_PASSWORD', ( $dsn['pass'] ?? null ) ); 121 | Config::define( 'DB_HOST', ( isset( $dsn['port'] ) ? "{$dsn['host']}:{$dsn['port']}" : $dsn['host'] ) ); 122 | } 123 | 124 | /** 125 | * Authentication Unique Keys and Salts 126 | * 127 | * @link https://api.wordpress.org/secret-key/1.1/salt/ WordPress.org secret-key service 128 | * @link https://roots.io/salts.html Roots' WordPress secret-key service 129 | */ 130 | Config::define( 'AUTH_KEY', env( 'AUTH_KEY' ) ); 131 | Config::define( 'SECURE_AUTH_KEY', env( 'SECURE_AUTH_KEY' ) ); 132 | Config::define( 'LOGGED_IN_KEY', env( 'LOGGED_IN_KEY' ) ); 133 | Config::define( 'NONCE_KEY', env( 'NONCE_KEY' ) ); 134 | Config::define( 'AUTH_SALT', env( 'AUTH_SALT' ) ); 135 | Config::define( 'SECURE_AUTH_SALT', env( 'SECURE_AUTH_SALT' ) ); 136 | Config::define( 'LOGGED_IN_SALT', env( 'LOGGED_IN_SALT' ) ); 137 | Config::define( 'NONCE_SALT', env( 'NONCE_SALT' ) ); 138 | 139 | /** 140 | * Custom Settings 141 | */ 142 | // Disable all automatic updates since WP is managed by Composer. 143 | Config::define( 'AUTOMATIC_UPDATER_DISABLED', true ); 144 | 145 | // Allow environment variable to control WP-Cron. 146 | Config::define( 'DISABLE_WP_CRON', ( env( 'DISABLE_WP_CRON' ) ?? false ) ); 147 | 148 | // Disable the plugin and theme file editor in the admin. 149 | Config::define( 'DISALLOW_FILE_EDIT', true ); 150 | 151 | // Disable plugin and theme updates and installation from the admin. 152 | Config::define( 'DISALLOW_FILE_MODS', true ); 153 | 154 | // Allow environment variable or environment type to control indexing of your site. 155 | Config::define( 'DISALLOW_INDEXING', ( env( 'DISALLOW_INDEXING' ) ?? ( WP_ENVIRONMENT_TYPE !== 'production' ) ) ); 156 | 157 | /** 158 | * Debugging Settings 159 | */ 160 | Config::define( 'WP_DEBUG_DISPLAY', false ); 161 | Config::define( 'WP_DEBUG_LOG', ( env( 'WP_DEBUG_LOG' ) ?? false ) ); 162 | Config::define( 'SCRIPT_DEBUG', false ); 163 | 164 | ini_set( 'display_errors', '0' ); 165 | 166 | /** 167 | * Prevent issues with PHP's CLI environement. 168 | * 169 | * @link https://make.wordpress.org/cli/handbook/guides/troubleshooting/#wordpress-configuration-file-wp-config-php 170 | */ 171 | if ( defined( 'WP_CLI' ) && WP_CLI && ! isset( $_SERVER['HTTP_HOST'] ) ) { 172 | $_SERVER['HTTP_HOST'] = 'host.local'; 173 | } 174 | 175 | /** 176 | * Allow WordPress to detect HTTPS when used behind 177 | * a reverse proxy or a load balancer. 178 | * 179 | * @link https://codex.wordpress.org/Function_Reference/is_ssl#Notes 180 | */ 181 | if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https' ) { 182 | $_SERVER['HTTPS'] = 'on'; 183 | } 184 | 185 | /** 186 | * Conditionally loads the environment-specific configuration file. 187 | * 188 | * @var string 189 | */ 190 | $env_conf = __DIR__ . '/environments/' . WP_ENVIRONMENT_TYPE . '.php'; 191 | if ( file_exists( $env_conf ) ) { 192 | require_once $env_conf; 193 | } 194 | 195 | /** 196 | * Defines all constants and throw an exception 197 | * if we are attempting to redefine a constant. 198 | */ 199 | Config::apply(); 200 | -------------------------------------------------------------------------------- /tools/patches/timber/timber/2.0/2725-fix-theme-path-parsing-for-image-generation.patch: -------------------------------------------------------------------------------- 1 | # 2 | # Fixes theme path parsing for image generation. 3 | # 4 | # Adds filters to customize the URL to path conversion to handle 5 | # non-standard theme directory structures. 6 | # 7 | # This patch is a subset of the changeset of timber/timber#2725. 8 | # 9 | # See: 10 | # - https://github.com/timber/timber/pull/2725 11 | # 12 | diff --git a/src/ImageHelper.php b/src/ImageHelper.php 13 | index e1421193..4c22d6be 100644 14 | --- a/src/ImageHelper.php 15 | +++ b/src/ImageHelper.php 16 | @@ -516,26 +516,63 @@ class ImageHelper 17 | * The image is expected to be either part of a theme, plugin, or an upload. 18 | * 19 | * @param string $url A URL (absolute or relative) pointing to an image. 20 | - * @return array An array (see keys in code below). 21 | + * @return array An array (see keys in code below). 22 | */ 23 | - public static function analyze_url($url) 24 | + public static function analyze_url(string $url): array 25 | + { 26 | + /** 27 | + * Filters whether to short-circuit the ImageHelper::analyze_url() 28 | + * file path of a URL located in a theme directory. 29 | + * 30 | + * Returning a non-null value from the filter will short-circuit 31 | + * ImageHelper::analyze_url(), returning that value. 32 | + * 33 | + * @since 2.0.0 34 | + * 35 | + * @param array|null $info The URL components array to short-circuit with. Default null. 36 | + * @param string $url The URL pointing to an image. 37 | + */ 38 | + $result = \apply_filters('timber/image_helper/pre_analyze_url', null, $url); 39 | + if (null === $result) { 40 | + $result = self::_analyze_url($url); 41 | + } 42 | + 43 | + /** 44 | + * Filters the array of anlayzed URL components. 45 | + * 46 | + * @since 2.0.0 47 | + * 48 | + * @param array $info The URL components. 49 | + * @param string $url The URL pointing to an image. 50 | + */ 51 | + return \apply_filters('timber/image_helper/analyze_url', $result, $url); 52 | + } 53 | + 54 | + /** 55 | + * Returns information about a URL. 56 | + * 57 | + * @param string $url A URL (absolute or relative) pointing to an image. 58 | + * @return array An array (see keys in code below). 59 | + */ 60 | + private static function _analyze_url(string $url): array 61 | { 62 | $result = [ 63 | - 'url' => $url, 64 | // the initial url 65 | - 'absolute' => URLHelper::is_absolute($url), 66 | + 'url' => $url, 67 | // is the url absolute or relative (to home_url) 68 | - 'base' => 0, 69 | + 'absolute' => URLHelper::is_absolute($url), 70 | // is the image in uploads dir, or in content dir (theme or plugin) 71 | - 'subdir' => '', 72 | + 'base' => 0, 73 | // the path between base (uploads or content) and file 74 | - 'filename' => '', 75 | + 'subdir' => '', 76 | // the filename, without extension 77 | - 'extension' => '', 78 | + 'filename' => '', 79 | // the file extension 80 | - 'basename' => '', 81 | + 'extension' => '', 82 | // full file name 83 | + 'basename' => '', 84 | ]; 85 | + 86 | $upload_dir = \wp_upload_dir(); 87 | $tmp = $url; 88 | if (\str_starts_with($tmp, ABSPATH) || \str_starts_with($tmp, '/srv/www/')) { 89 | @@ -567,6 +604,7 @@ class ImageHelper 90 | $result['filename'] = $parts['filename']; 91 | $result['extension'] = \strtolower($parts['extension']); 92 | $result['basename'] = $parts['basename']; 93 | + 94 | return $result; 95 | } 96 | 97 | @@ -576,16 +614,52 @@ class ImageHelper 98 | * @param string $src A URL (http://example.org/wp-content/themes/twentysixteen/images/home.jpg). 99 | * @return string Full path to the file in question. 100 | */ 101 | - public static function theme_url_to_dir($src) 102 | + public static function theme_url_to_dir(string $src): string 103 | + { 104 | + /** 105 | + * Filters whether to short-circuit the ImageHelper::theme_url_to_dir() 106 | + * file path of a URL located in a theme directory. 107 | + * 108 | + * Returning a non-null value from the filter will short-circuit 109 | + * ImageHelper::theme_url_to_dir(), returning that value. 110 | + * 111 | + * @since 2.0.0 112 | + * 113 | + * @param string|null $path Full path to short-circuit with. Default null. 114 | + * @param string $src The URL to be converted. 115 | + */ 116 | + $path = \apply_filters('timber/image_helper/pre_theme_url_to_dir', null, $src); 117 | + if (null === $path) { 118 | + $path = self::_theme_url_to_dir($src); 119 | + } 120 | + 121 | + /** 122 | + * Filters the raw file path of a URL located in a theme directory. 123 | + * 124 | + * @since 2.0.0 125 | + * 126 | + * @param string $path The resolved full path to $src. 127 | + * @param string $src The URL that was converted. 128 | + */ 129 | + return \apply_filters('timber/image_helper/theme_url_to_dir', $path, $src); 130 | + } 131 | + 132 | + /** 133 | + * Converts a URL located in a theme directory into the raw file path. 134 | + * 135 | + * @param string $src A URL (http://example.org/wp-content/themes/twentysixteen/images/home.jpg). 136 | + * @return string Full path to the file in question. 137 | + */ 138 | + private static function _theme_url_to_dir(string $src): string 139 | { 140 | $site_root = \trailingslashit(\get_theme_root_uri()) . \get_stylesheet(); 141 | - $tmp = \str_replace($site_root, '', $src); 142 | - //$tmp = trailingslashit(get_theme_root()).get_stylesheet().$tmp; 143 | - $tmp = \get_stylesheet_directory() . $tmp; 144 | - if (\realpath($tmp)) { 145 | - return \realpath($tmp); 146 | + $path = \str_replace($site_root, '', $src); 147 | + //$path = \trailingslashit(\get_theme_root()).\get_stylesheet().$path; 148 | + $path = \get_stylesheet_directory() . $path; 149 | + if ($_path = \realpath($path)) { 150 | + return $_path; 151 | } 152 | - return $tmp; 153 | + return $path; 154 | } 155 | 156 | /** 157 | @@ -795,8 +869,8 @@ class ImageHelper 158 | } 159 | } 160 | 161 | - // -- the below methods are just used for unit testing the URL generation code 162 | - // 163 | + //-- the below methods are just used for 164 | + // unit testing the URL generation code --// 165 | /** 166 | * @internal 167 | */ 168 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚂 WordPress Project Boilerplate 2 | 3 | A quick and opinionated WordPress boilerplate with Composer, 4 | an easier configuration, and an improved folder structure. 5 | 6 | > This boilerplate is based on [wp-jazz/wp-project-skeleton] 7 | > which is derived from [Bedrock][roots/bedrock]. 8 | > 9 | > If you have the capability, please consider 10 | > [sponsoring Roots](https://github.com/sponsors/roots). 11 | 12 | ## Overview 13 | 14 | This boilerplate assumes you are familiar with [wp-jazz/wp-project-skeleton] 15 | and [Bedrock](https://docs.roots.io/bedrock/master/installation/). 16 | 17 | Differences with [wp-jazz/wp-project-skeleton]: 18 | 19 | * The _Web root directory_ is `www` instead of `public`. 20 | * Includes a copy of [`wp-ajax.php`](www/wp-ajax.php), a near-identical copy 21 | of WordPress' [`admin-ajax.php`](https://github.com/WordPress/WordPress/blob/6.1.0/wp-admin/admin-ajax.php). 22 | * Prepared for integration with: 23 | * [Activity Log][pojome/activity-log] — Plugin to monitor and log all changes and activities. 24 | * [Advanced Custom Fields Pro][acf] — Plugin to allow adding extra content fields. 25 | * [Ecocide][mcaskill/wp-ecocide] — Library to disable basic features of WordPress. 26 | * [Gravity Forms][gravityforms] — Plugin to allow building custom forms. 27 | * [Polylang Pro][polylang] — Plugin to support multilingual content. 28 | * Includes copies of WordPress databases: 29 | * Unilingual (English) 30 | * Multilingual (English and French) 31 | 32 | ## Requirements 33 | 34 | * PHP >= 7.4 35 | * Composer ([Installation](https://getcomposer.org/doc/00-intro.md#installation-linux-unix-osx)) 36 | * Active licenses for Advanced Custom Fields Pro, Gravity Forms, and Polylang Pro. 37 | 38 | ## Installation 39 | 40 | 1. Create a new project: 41 | 42 | ```shell 43 | composer create-project locomotivemtl/wordpress-boilerplate 44 | ``` 45 | 46 | Note that installation of Composer dependencies will fail because 47 | of the premium WordPress plugins that require license keys to be defined. 48 | 49 | Alternatively, clone the repository: 50 | 51 | ```shell 52 | git clone https://github.com/locomotivemtl/wordpress-boilerplate.git . 53 | rm -rf .git 54 | git init 55 | git add -A 56 | git commit -m "Initial commit" 57 | ``` 58 | 59 | Or add the repository as a remote: 60 | 61 | ```shell 62 | git remote add boilerplate https://github.com/locomotivemtl/wordpress-boilerplate.git 63 | git fetch boilerplate main 64 | git merge boilerplate/main 65 | ``` 66 | 67 | 2. Update environment variables in the `.env` file. 68 | 69 | Wrap values that may contain non-alphanumeric characters with quotes, 70 | or they may be incorrectly parsed. 71 | 72 | * Database variables: 73 | * `DB_NAME` — Database name 74 | * `DB_USER` — Database user 75 | * `DB_PASSWORD` — Database password 76 | * `DB_HOST` — Database host 77 | * Optionally, you can define `DATABASE_URL` for using a DSN instead of 78 | using the variables above (e.g. `mysql://user:password@127.0.0.1:3306/db_name`) 79 | * `WP_DEVELOPMENT_MODE` — Set the development mode (`all` for development or empty string for production) 80 | * `WP_ENVIRONMENT_TYPE` — Set to environment (`development`, `staging`, `production`) 81 | * `WP_HOME` — Full URL to WordPress home (https://example.com) 82 | * `WP_SITEURL` — Avoid editing this variable. Full URL to WordPress including subdirectory (https://example.com/wordpress) 83 | * `ACF_PRO_KEY`, `GRAVITY_FORMS_KEY`, `POLYLANG_PRO_KEY` — Premium plugin license keys. 84 | * `AUTH_KEY`, `SECURE_AUTH_KEY`, `LOGGED_IN_KEY`, `NONCE_KEY`, `AUTH_SALT`, `SECURE_AUTH_SALT`, `LOGGED_IN_SALT`, `NONCE_SALT` 85 | * Generate with [wp-cli-dotenv-command] 86 | * Generate with [our WordPress salts generator][roots/salts] 87 | 88 | 3. Supply Composer with credentials for authenticating the installation of Polylang Pro: 89 | 90 | This step is necessary because Polylang Pro uses 91 | [Easy Digital Downloads][easydigitaldownloads] (EDD) for distribution. 92 | 93 | ```sh 94 | composer config [--global] --editor --auth 95 | ``` 96 | 97 | ```json 98 | { 99 | "http-basic": { 100 | "polylang.pro": { 101 | "username": "username", 102 | "password": "password" 103 | } 104 | } 105 | } 106 | ``` 107 | 108 | 4. Add plugin(s) in `www/plugins` and `www/mu-plugins`, and theme(s) in `www/themes` either: 109 | * as you would for a normal WordPress site (add an exception to the `.gitignore` if you want to index them) 110 | * or as Composer dependencies. 111 | 112 | 5. Most projects use pretty permalinks. This requires a `.htaccess` file on 113 | Apache servers. This file is not indexed in Git since it can contain 114 | environment-specific requirements. To create or update the file (and update 115 | rewrite rules in the database): 116 | 117 | ```shell 118 | wp rewrite flush --hard 119 | ``` 120 | 121 | 6. Set the document root on your Web server to Jazz's `www` folder: `/path/to/site/www/`. 122 | 123 | 7. Access WordPress admin at `https://example.com/wordpress/wp-admin/`. 124 | 125 | If you choose to use one of the starting databases, you will need to change the 126 | following: 127 | 128 | * Replace the base URI: 129 | * `example.test` 130 | * Add your license keys: 131 | * `acf_pro_license` 132 | * `rg_gforms_key` 133 | * `rg_gforms_captcha_public_key` 134 | * `rg_gforms_captcha_private_key` 135 | 136 | 137 | 138 | 139 | 140 | ## Contributing 141 | 142 | Contributions are welcome from everyone. 143 | We have [contributing guidelines](CONTRIBUTING.md) 144 | to help you get started. 145 | 146 | ## Acknowledgements 147 | 148 | This boilerplate is based on the solid work of many that have come before me, including: 149 | 150 | * [Bedrock][roots/bedrock] 151 | * [wp-jazz/wp-project-skeleton] 152 | 153 | [acf]: https://advancedcustomfields.com 154 | [composer]: https://getcomposer.org 155 | [easydigitaldownloads]: https://easydigitaldownloads.com/ 156 | [gravityforms]: https://gravityforms.com 157 | [mcaskill/wp-ajax]: https://gist.github.com/mcaskill/95acb103a5e5a78a7184b38fbacfa66e 158 | [mcaskill/wp-ecocide]: https://github.com/mcaskill/wp-ecocide 159 | [pojome/activity-log]: https://github.com/pojome/activity-log 160 | [polylang]: https://polylang.pro 161 | [roots/bedrock]: https://github.com/roots/bedrock 162 | [roots/salts]: https://roots.io/salts.html 163 | [roots/wp-password-bcrypt]: https://github.com/roots/wp-password-bcrypt 164 | [vlucas/phpdotenv]: https://github.com/vlucas/phpdotenv 165 | [wp-cli-dotenv-command]: https://github.com/aaemnnosttv/wp-cli-dotenv-command 166 | [wp-jazz/wp-project-skeleton]: https://github.com/wp-jazz/wp-project-skeleton 167 | -------------------------------------------------------------------------------- /www/themes/boilerplate-theme/includes/Site.php: -------------------------------------------------------------------------------- 1 | theme_uri = $theme->get_stylesheet_directory_uri(); 41 | $this->theme_path = $theme->get_stylesheet_directory(); 42 | 43 | $this->assets_uri = Path::canonicalize( $this->theme_uri . '/' . THEME_ASSETS_DIR ); 44 | $this->assets_path = realpath( $this->theme_path . '/' . THEME_ASSETS_DIR ); 45 | 46 | $this->register_actions(); 47 | $this->register_filters(); 48 | 49 | parent::__construct(); 50 | } 51 | 52 | /** 53 | * Clean up wordpress stuff 54 | * 55 | * @todo 56 | * 57 | * @listens action:init 58 | * 59 | * @return void 60 | */ 61 | public function cleanup_wordpress(): void 62 | { 63 | remove_action('wp_head', 'print_emoji_detection_script', 7); 64 | remove_action('admin_print_scripts', 'print_emoji_detection_script'); 65 | remove_action('wp_print_styles', 'print_emoji_styles'); 66 | remove_action('admin_print_styles', 'print_emoji_styles'); 67 | } 68 | 69 | /** 70 | * Enqueue theme scripts 71 | * 72 | * @listens action:wp_enqueue_scripts 73 | */ 74 | public function enqueue_theme_assets(): void 75 | { 76 | // Disable core block styles. 77 | wp_dequeue_style('wp-block-library'); 78 | 79 | // Enqueue theme scripts. 80 | wp_enqueue_script('theme-vendors', $this->get_assets_uri('scripts/vendors.js'), null, THEME_VERSION); 81 | wp_script_add_data('theme-vendors', 'strategy', 'defer'); 82 | 83 | wp_enqueue_script('theme-app', $this->get_assets_uri('scripts/app.js'), null, THEME_VERSION); 84 | wp_script_add_data('theme-app', 'strategy', 'defer'); 85 | 86 | // Enqueue theme styles. 87 | wp_enqueue_style('theme-critical', $this->get_assets_uri('styles/critical.css'), null, THEME_VERSION); 88 | wp_style_add_data('theme-critical', 'critical', 'inline'); 89 | 90 | wp_enqueue_style('theme-app', $this->get_assets_uri('styles/main.css'), ['theme-critical'], THEME_VERSION); 91 | wp_style_add_data('theme-app', 'critical', 'delay'); 92 | } 93 | 94 | /** 95 | * Filters the HTML script tag of an enqueued script. 96 | * 97 | * This function cleans up the output of `)!i', $attr, $html, 1 ); 156 | } 157 | } // end foreach 158 | 159 | return $html; 160 | } 161 | 162 | /** 163 | * Filters the HTML link tag of an enqueued style. 164 | * 165 | * This function cleans up the output of stylesheet `` tags and 166 | * also inlines or delays any CSS via the "critical" style data. 167 | * 168 | * @listens filter:style_loader_tag 169 | * 170 | * @param string $html The link tag for the enqueued style. 171 | * @param string $handle The style's registered handle. 172 | * @return string 173 | */ 174 | public function filter_style_loader_tag( string $html, string $handle ) : string { 175 | preg_match_all( 176 | "!!", 177 | $html, 178 | $matches 179 | ); 180 | 181 | if ( empty( $matches[2][0] ) ) { 182 | return $html; 183 | } 184 | 185 | $src = $matches[2][0]; 186 | 187 | $critical = wp_styles()->get_data( $handle, 'critical' ); 188 | 189 | if ( 'inline' === $critical ) { 190 | $path = str_replace( ThemeHelper::get_stylesheet_directory_uri(), '', $src ); 191 | $path = ThemeHelper::get_stylesheet_directory() . strtok( $path, '?' ); 192 | 193 | if ( is_readable( $path ) ) { 194 | $css = file_get_contents( $path ); 195 | 196 | if ( ! empty( $css ) ) { 197 | return sprintf( 198 | "\n", 199 | esc_attr( $handle ), 200 | $css 201 | ); 202 | } 203 | } 204 | } 205 | 206 | $media = ''; 207 | $onload = ''; 208 | 209 | if ( ! empty( $matches[3][0] ) ) { 210 | $_media = $matches[3][0]; 211 | 212 | if ( 'delay' === $critical ) { 213 | // Assign the real media type when the stylesheet is loaded. 214 | $media = ' media="print"'; 215 | $onload = ' onload="this.media=\'' . $_media . '\'; this.onload=null; this.isLoaded=true"'; 216 | } elseif ( $_media !== 'all' ) { 217 | // Only display media if it is meaningful. 218 | $media = ' media="' . $_media . '"'; 219 | } 220 | } 221 | 222 | return '' . "\n"; 223 | } 224 | 225 | /** 226 | * Filters the global Timber context. 227 | * 228 | * @listens filter:timber/context 229 | * 230 | * @param array $context The global context. 231 | * @return array 232 | */ 233 | public function filter_timber_context(array $context) 234 | { 235 | $context['menu'] = Timber::get_menu(); 236 | $context['site'] = $this; 237 | 238 | return $context; 239 | } 240 | 241 | /** 242 | * Adds extension to Twig. 243 | * 244 | * @listens filter:timber/twig 245 | * 246 | * @param TwigEnvironment $twig The Twig Environment to which 247 | * you can add additional functionality. 248 | * @return TwigEnvironment 249 | */ 250 | public function filter_twig_environment(TwigEnvironment $twig): TwigEnvironment 251 | { 252 | $twig->addExtension(new \Twig\Extension\StringLoaderExtension()); 253 | $twig->addExtension(new \Twig\Extra\Html\HtmlExtension()); 254 | 255 | $twig->addGlobal('debug', (wp_get_environment_type() === 'development')); 256 | 257 | $twig->addGlobal('assets_uri', $this->assets_uri); 258 | $twig->addGlobal('assets_path', $this->assets_path); 259 | 260 | $twig->addGlobal('theme_uri', $this->theme_uri); 261 | $twig->addGlobal('theme_path', $this->theme_path); 262 | 263 | return $twig; 264 | } 265 | 266 | /** 267 | * Adds filters to Twig. 268 | * 269 | * @listens filter:timber/twig/filters 270 | * 271 | * @param array $filters An associative array of Twig filter definitions. 272 | * @return array 273 | */ 274 | public function filter_twig_filters(array $filters): array 275 | { 276 | $filters['assets_path'] = [ 277 | 'callable' => [$this, 'get_assets_path'], 278 | ]; 279 | $filters['assets_uri'] = [ 280 | 'callable' => [$this, 'get_assets_uri'], 281 | ]; 282 | $filters['themes_path'] = [ 283 | 'callable' => [$this, 'get_themes_path'], 284 | ]; 285 | $filters['themes_uri'] = [ 286 | 'callable' => [$this, 'get_themes_uri'], 287 | ]; 288 | 289 | return $filters; 290 | } 291 | 292 | /** 293 | * Retrieves the path to the active theme's static assets. 294 | * 295 | * @fires filter:app/theme/assets_path 296 | * 297 | * @param string|null $to Optional. Path relative to the assets path. 298 | * Default empty. 299 | * @return string Theme path with optional path appended. 300 | */ 301 | public function get_assets_path($to = null): string 302 | { 303 | $assets_to = THEME_ASSETS_DIR; 304 | 305 | if ($to && is_string($to)) { 306 | $assets_to .= '/' . ltrim($to, '/'); 307 | } 308 | 309 | $path = $this->get_theme_path($assets_to); 310 | 311 | /** 312 | * Filters the assets path. 313 | * 314 | * @event filter:app/theme/assets_path 315 | * 316 | * @param string $path The absolute assets path including given path. 317 | * @param string|null $to Path relative to the assets path. 318 | * NULL if no path is specified. 319 | */ 320 | return apply_filters('app/theme/assets_path', $path, $to); 321 | } 322 | 323 | /** 324 | * Retrieves the URL to the active theme's static assets. 325 | * 326 | * @fires filter:app/theme/assets_url 327 | * 328 | * @param string|null $path Optional. Path relative to the assets URL. 329 | * Default empty. 330 | * @param string|null $scheme Optional. Scheme to give the assets URL context. 331 | * Accepts 'http', 'https', 'relative', 'rest', or null. Default null. 332 | * @return string Theme URL with optional path appended. 333 | */ 334 | public function get_assets_uri($path = null, $scheme = null): string 335 | { 336 | $orig_scheme = $scheme; 337 | 338 | $assets_path = THEME_ASSETS_DIR; 339 | 340 | if ($path && is_string($path)) { 341 | $assets_path .= '/' . ltrim($path, '/'); 342 | } 343 | 344 | $url = $this->get_theme_uri($assets_path, $scheme); 345 | 346 | /** 347 | * Filters the assets URL. 348 | * 349 | * @event filter:app/theme/assets_url 350 | * 351 | * @param string $url The complete assets URL including scheme and path. 352 | * @param string|null $path Path relative to the assets URL. 353 | * NULL if no path is specified. 354 | * @param string|null $orig_scheme Scheme to give the assets URL context. 355 | * Accepts 'http', 'https', 'relative', 'rest', or NULL. 356 | */ 357 | return apply_filters('app/theme/assets_url', $url, $path, $orig_scheme); 358 | } 359 | 360 | /** 361 | * Retrieves the path to the active theme. 362 | * 363 | * @fires filter:app/theme/theme_path 364 | * 365 | * @param string|null $to Optional. Path relative to the theme path. 366 | * Default empty. 367 | * @return string Theme path with optional path appended. 368 | */ 369 | public function get_theme_path($to = null): string 370 | { 371 | $path = $this->theme_path; 372 | 373 | if ($to && is_string($to)) { 374 | $path .= realpath('/' . ltrim($to, '/')); 375 | } 376 | 377 | /** 378 | * Filters the theme path. 379 | * 380 | * @event filter:app/theme/theme_path 381 | * 382 | * @param string $path The absolute theme path including given path. 383 | * @param string|null $to Path relative to the theme path. 384 | * NULL if no path is specified. 385 | */ 386 | return apply_filters('app/theme/theme_path', $path, $to); 387 | } 388 | 389 | /** 390 | * Retrieves the URL to the active theme. 391 | * 392 | * @fires filter:app/theme/theme_url 393 | * 394 | * @param string|null $path Optional. Path relative to the theme URL. 395 | * Default empty. 396 | * @param string|null $scheme Optional. Scheme to give the theme URL context. 397 | * Accepts 'http', 'https', 'relative', 'rest', or null. Default null. 398 | * @return string Theme URL with optional path appended. 399 | */ 400 | public function get_theme_uri($path = null, $scheme = null): string 401 | { 402 | $orig_scheme = $scheme; 403 | 404 | $base_url = $this->theme_uri; 405 | 406 | if (!in_array($scheme, ['http', 'https', 'relative'], true)) { 407 | if (is_ssl()) { 408 | $scheme = 'https'; 409 | } else { 410 | $scheme = parse_url($base_url, PHP_URL_SCHEME); 411 | } 412 | } 413 | 414 | $url = set_url_scheme($base_url, $scheme); 415 | 416 | if ($path && is_string($path)) { 417 | $url .= '/' . ltrim($path, '/'); 418 | } 419 | 420 | $url = Path::canonicalize($url); 421 | 422 | /** 423 | * Filters the theme URL. 424 | * 425 | * @event filter:app/theme/theme_url 426 | * 427 | * @param string $url The complete theme URL including scheme and path. 428 | * @param string|null $path Path relative to the theme URL. 429 | * NULL if no path is specified. 430 | * @param string|null $orig_scheme Scheme to give the theme URL context. 431 | * Accepts 'http', 'https', 'relative', 'rest', or NULL. 432 | */ 433 | return apply_filters('app/theme/theme_url', $url, $path, $orig_scheme); 434 | } 435 | 436 | /** 437 | * Registers action hooks. 438 | */ 439 | public function register_actions(): void 440 | { 441 | add_action('after_setup_theme', [$this, 'register_theme_features']); 442 | add_action('init', [$this, 'cleanup_wordpress']); 443 | add_action('init', [$this, 'register_post_types']); 444 | add_action('init', [$this, 'register_taxonomies']); 445 | add_action('wp_enqueue_scripts', [$this, 'enqueue_theme_assets']); 446 | add_action('acf/init', [$this, 'register_acf_settings']); 447 | } 448 | 449 | /** 450 | * Registers ACF settings. 451 | * 452 | * @todo Registration should occur in a mu-plugin. 453 | */ 454 | public function register_acf_settings() 455 | { 456 | acf_update_setting('acfe/modules/single_meta', true); 457 | } 458 | 459 | /** 460 | * Registers filter hooks. 461 | */ 462 | public function register_filters(): void 463 | { 464 | add_filter('timber/context', [$this, 'filter_timber_context']); 465 | add_filter('timber/twig', [$this, 'filter_twig_environment']); 466 | add_filter('timber/twig/filters', [$this, 'filter_twig_filters']); 467 | 468 | /** 469 | * @todo https://locomotivemtl.teamwork.com/#/tasks/34821234 470 | */ 471 | if (!is_admin()) { 472 | add_filter('style_loader_tag', [$this, 'filter_style_loader_tag'], 15, 2); 473 | add_filter('script_loader_tag', [$this, 'filter_script_loader_tag'], 15, 2); 474 | } 475 | } 476 | 477 | /** 478 | * Registers custom post types. 479 | * 480 | * @todo Registration should occur in a mu-plugin. 481 | */ 482 | public function register_post_types() 483 | { 484 | } 485 | 486 | /** 487 | * Registers custom taxonomies. 488 | * 489 | * @todo Registration should occur in a mu-plugin. 490 | */ 491 | public function register_taxonomies() 492 | { 493 | } 494 | 495 | /** 496 | * Registers theme support for given features. 497 | * 498 | * @listens action:after_setup_theme 499 | */ 500 | public function register_theme_features(): void 501 | { 502 | // Add default posts and comments RSS feed links to head. 503 | add_theme_support('automatic-feed-links'); 504 | 505 | /* 506 | * Let WordPress manage the document title. 507 | * By adding theme support, we declare that this theme does not use a 508 | * hard-coded tag in the document head, and expect WordPress to 509 | * provide it for us. 510 | */ 511 | add_theme_support('title-tag'); 512 | 513 | /* 514 | * Enable support for Post Thumbnails on posts and pages. 515 | * 516 | * @link https://developer.wordpress.org/themes/functionality/featured-images-post-thumbnails/ 517 | */ 518 | add_theme_support('post-thumbnails'); 519 | 520 | /* 521 | * Switch default core markup for search form, comment form, and comments 522 | * to output valid HTML5. 523 | */ 524 | add_theme_support( 525 | 'html5', 526 | [ 527 | 'comment-form', 528 | 'comment-list', 529 | 'gallery', 530 | 'caption', 531 | ] 532 | ); 533 | 534 | /* 535 | * Enable support for Post Formats. 536 | * 537 | * See: https://codex.wordpress.org/Post_Formats 538 | */ 539 | add_theme_support( 540 | 'post-formats', 541 | [ 542 | 'aside', 543 | 'image', 544 | 'video', 545 | 'quote', 546 | 'link', 547 | 'gallery', 548 | 'audio', 549 | ] 550 | ); 551 | 552 | add_theme_support('menus'); 553 | } 554 | } 555 | --------------------------------------------------------------------------------