From c82e11604ff013c92716457968c0ef7656e5fe4f Mon Sep 17 00:00:00 2001 From: Warwick Date: Tue, 14 Apr 2026 15:02:46 +0200 Subject: [PATCH 1/6] feat: Refactor linkable blocks to new structure and add frontend functionality --- {assets => src}/css/linkable-blocks.css | 0 {assets => src}/js/linkable-blocks-editor.js | 0 {assets => src}/js/linkable-blocks-frontend.js | 0 webpack.config.cjs | 6 +++--- 4 files changed, 3 insertions(+), 3 deletions(-) rename {assets => src}/css/linkable-blocks.css (100%) rename {assets => src}/js/linkable-blocks-editor.js (100%) rename {assets => src}/js/linkable-blocks-frontend.js (100%) diff --git a/assets/css/linkable-blocks.css b/src/css/linkable-blocks.css similarity index 100% rename from assets/css/linkable-blocks.css rename to src/css/linkable-blocks.css diff --git a/assets/js/linkable-blocks-editor.js b/src/js/linkable-blocks-editor.js similarity index 100% rename from assets/js/linkable-blocks-editor.js rename to src/js/linkable-blocks-editor.js diff --git a/assets/js/linkable-blocks-frontend.js b/src/js/linkable-blocks-frontend.js similarity index 100% rename from assets/js/linkable-blocks-frontend.js rename to src/js/linkable-blocks-frontend.js diff --git a/webpack.config.cjs b/webpack.config.cjs index ad33f8a..b82752b 100644 --- a/webpack.config.cjs +++ b/webpack.config.cjs @@ -12,9 +12,9 @@ module.exports = { 'js/back-to-top-view': path.resolve( process.cwd(), 'src/plugins/back-to-top', 'view.js' ), 'css/back-to-top': path.resolve( process.cwd(), 'src/plugins/back-to-top', 'style.scss' ), 'js/style-switcher': path.resolve( process.cwd(), 'src/js', 'style-switcher.js' ), - 'js/linkable-blocks-editor': path.resolve( process.cwd(), 'assets/js', 'linkable-blocks-editor.js' ), - 'js/linkable-blocks-frontend': path.resolve( process.cwd(), 'assets/js', 'linkable-blocks-frontend.js' ), - 'css/linkable-blocks': path.resolve( process.cwd(), 'assets/css', 'linkable-blocks.css' ), + 'js/linkable-blocks-editor': path.resolve( process.cwd(), 'src/js', 'linkable-blocks-editor.js' ), + 'js/linkable-blocks-frontend': path.resolve( process.cwd(), 'src/js', 'linkable-blocks-frontend.js' ), + 'css/linkable-blocks': path.resolve( process.cwd(), 'src/css', 'linkable-blocks.css' ), } ), output: { ...defaultConfig.output, From d6867a86db4d277a5f69a6ef81414b996683ac48 Mon Sep 17 00:00:00 2001 From: Warwick Date: Tue, 14 Apr 2026 15:09:06 +0200 Subject: [PATCH 2/6] feat: Update asset organization and build instructions in documentation --- .github/copilot-instructions.md | 13 ++++++++-- .github/instructions/assets.instructions.md | 26 +++++++++++-------- .../plugin-structure.instructions.md | 6 +++-- AGENTS.md | 12 ++++++--- 4 files changed, 38 insertions(+), 19 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d21d862..7d3ea45 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -31,8 +31,9 @@ This is the LightSpeed Site Plugin — custom blocks and site-specific functiona | Main plugin bootstrap | `ls-plugin.php` | | PHP includes | `inc/` | | Block source files | `src/blocks/` | -| Built block assets | `blocks/` | -| Static assets | `assets/` | +| JS/CSS source files | `src/js/`, `src/css/` | +| Built CSS/JS assets | `build/` | +| Static non-compiled assets | `assets/` | | Block patterns | `patterns/` | | Translation files | `languages/` | | End-user docs | `docs/` | @@ -69,12 +70,20 @@ See `.github/instructions/` for detailed guidance: ## Validation and linting ```bash +npm run build # Build all source JS/CSS assets from src/ into build/ npm run plugin:validate # Validate plugin structure npm run security:scan # PHP security scan composer run phpcs # PHP coding standards npm run lint # JS + CSS + JSON linting ``` +## Asset build rule + +- Add authored JS and CSS files under `src/`. +- Run `npm run build` after changing JS or CSS. +- Enqueue or include only files from `build/` in PHP. +- Do not treat `assets/js/` or `assets/css/` as source directories. + --- ## Accessibility diff --git a/.github/instructions/assets.instructions.md b/.github/instructions/assets.instructions.md index 83dba51..859308b 100644 --- a/.github/instructions/assets.instructions.md +++ b/.github/instructions/assets.instructions.md @@ -8,22 +8,24 @@ applyTo: "assets/**,src/**" | Folder | Content | | ---------------- | ------------------------------------------------------ | -| `assets/css/` | Static (pre-built) CSS files for frontend or admin | -| `assets/js/` | Static (pre-built) JS files not managed by block build | +| `assets/css/` | Static non-source assets only; do not add authored CSS source here | +| `assets/js/` | Static non-source assets only; do not add authored JS source here | | `assets/images/` | Plugin images (logos, backgrounds, etc.) | | `assets/icons/` | SVG or PNG icons | | `src/blocks/` | Block source files — compiled by `@wordpress/scripts` | -| `src/css/` | Non-block CSS source files | -| `src/js/` | Non-block JS source files | -| `blocks/` | Built block assets — output of `npm run build` | +| `src/css/` | Non-block CSS source files for the build pipeline | +| `src/js/` | Non-block JS source files for the build pipeline | +| `build/` | Built CSS/JS assets output by `npm run build` | ## Rules - Do not mix source and built files in the same folder. +- Put authored JS and CSS source files in `src/`, not `assets/`. - `src/` contains files that need compilation. -- `assets/` contains files that are already production-ready. -- `blocks/` contains built block output from `@wordpress/scripts`. -- Do not commit compiled output from `src/` — use `npm run build` to generate it. +- `build/` contains the generated files that are enqueued or included by PHP. +- Treat `assets/` as static non-compiled assets only, such as images or icons. +- After changing JS or CSS in `src/`, run `npm run build` to regenerate the `build/` output. +- Do not enqueue or include JS/CSS directly from `src/`. ## WordPress preset syntax @@ -32,20 +34,22 @@ applyTo: "assets/**,src/**" ## Enqueuing assets in PHP -Use `wp_enqueue_style()` and `wp_enqueue_script()` with versioning: +Use `wp_enqueue_style()` and `wp_enqueue_script()` with versioning, and point them at built files: ```php wp_enqueue_style( 'ls-plugin-frontend', - LS_PLUGIN_PLUGIN_URL . 'assets/css/frontend.css', + LS_PLUGIN_PLUGIN_URL . 'build/css/frontend.css', [], LS_PLUGIN_VERSION ); ``` +When webpack generates `*.asset.php` metadata files, use them for dependencies and versions. + ## Block assets -Block assets (editor and frontend CSS/JS) are declared in `block.json` and enqueued automatically by `register_block_type()`. +Block assets (editor and frontend CSS/JS) are declared in `block.json` and built into `build/`, then enqueued automatically by `register_block_type()`. Do not manually enqueue block scripts — let `block.json` handle it. ## Image and icon guidelines diff --git a/.github/instructions/plugin-structure.instructions.md b/.github/instructions/plugin-structure.instructions.md index 7b32d72..4fc34ae 100644 --- a/.github/instructions/plugin-structure.instructions.md +++ b/.github/instructions/plugin-structure.instructions.md @@ -31,8 +31,8 @@ applyTo: "**" |---|---| | `inc/` | Optional PHP include files — loaded from main plugin file | | `src/` | Source files for compilation (blocks, CSS, JS) | -| `blocks/` | Built block assets — output of `npm run build` | -| `assets/` | Static (pre-built) CSS, JS, images, icons | +| `build/` | Built CSS/JS assets — output of `npm run build` | +| `assets/` | Static non-compiled assets such as images and icons | | `patterns/` | WordPress block patterns (PHP with header comments) | | `templates/` | Optional block templates and template parts | | `languages/` | Translation files (.pot, .po, .mo) | @@ -51,6 +51,8 @@ Plugin-specific values are already set for this repo: - Do not put developer reports in `docs/`. - Do not put built assets in `src/`. +- Do not add authored JS or CSS source files to `assets/`. +- Do not enqueue raw files from `src/`; build them first and include the generated files from `build/`. - Do not add Playwright, Storybook, Docker, webpack config, or Vite. - Do not add a PHP autoloader unless there is a genuine reason. - Do not add issue templates or pull request templates. diff --git a/AGENTS.md b/AGENTS.md index e402d97..7a18d5b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,8 +36,8 @@ Responsibilities: ├── readme.txt Lightweight WordPress distribution placeholder ├── inc/ PHP include files (optional, loaded from main plugin file) ├── src/ Source files: src/blocks/, src/css/, src/js/ -├── blocks/ Built or registered block asset directories -├── assets/ Static assets: assets/css/, assets/js/, assets/images/, assets/icons/ +├── build/ Built CSS/JS assets generated by npm build +├── assets/ Static non-compiled assets: images, icons, other fixed files ├── patterns/ WordPress block patterns (PHP files with header comments) ├── templates/ Optional block templates and template parts ├── languages/ Translation files (.pot, .po, .mo) @@ -137,11 +137,15 @@ register_block_type( LS_PLUGIN_PLUGIN_DIR . 'blocks/my-block' ); ## Asset Conventions -- **Static assets** (ready-to-use CSS, JS, images, icons): `assets/` - **Source files** (need compilation): `src/` -- **Built block assets**: `blocks/` +- **Built CSS/JS assets**: `build/` +- **Static non-compiled assets** (images, icons, fixed files): `assets/` - Do not mix source and built assets in the same folder. - Frontend CSS and editor CSS should be separate files where appropriate. +- Add authored JS and CSS to `src/js/`, `src/css/`, or `src/blocks/`. +- Run `npm run build` after changing JS or CSS source files. +- PHP must enqueue or include built assets from `build/`, never raw source files from `src/`. +- Do not add authored JS or CSS source files to `assets/js/` or `assets/css/`. --- From 01d3909152c5eaccd00e5199e27c7c75be193a8e Mon Sep 17 00:00:00 2001 From: Warwick Date: Tue, 14 Apr 2026 16:30:10 +0200 Subject: [PATCH 3/6] feat: Implement SCF JSON handling with validation and configuration classes --- .github/schemas/scf-field-group.schema.json | 91 ++++ inc/class-scf-json-validator.php | 435 ++++++++++++++++++++ inc/class-scf-json.php | 228 ++++++++++ ls-plugin.php | 20 + scf-json/.gitkeep | 0 scf-json/group_ls_plugin_example.json | 27 ++ 6 files changed, 801 insertions(+) create mode 100644 .github/schemas/scf-field-group.schema.json create mode 100644 inc/class-scf-json-validator.php create mode 100644 inc/class-scf-json.php create mode 100644 scf-json/.gitkeep create mode 100644 scf-json/group_ls_plugin_example.json diff --git a/.github/schemas/scf-field-group.schema.json b/.github/schemas/scf-field-group.schema.json new file mode 100644 index 0000000..9d5c2fd --- /dev/null +++ b/.github/schemas/scf-field-group.schema.json @@ -0,0 +1,91 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://lightspeedwp.agency/schemas/scf-field-group.schema.json", + "title": "SCF Field Group", + "description": "Schema for Secure Custom Fields field group JSON exports.", + "type": "object", + "required": [ + "key", + "title", + "fields", + "location" + ], + "properties": { + "key": { + "type": "string", + "pattern": "^group_" + }, + "title": { + "type": "string" + }, + "fields": { + "type": "array", + "items": { + "type": "object", + "required": ["key", "type"], + "properties": { + "key": { + "type": "string" + }, + "type": { + "type": "string" + }, + "name": { + "type": "string" + }, + "label": { + "type": "string" + }, + "sub_fields": { + "type": "array" + }, + "layouts": { + "type": "array" + } + }, + "additionalProperties": true + } + }, + "location": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "object", + "required": ["param", "operator", "value"], + "properties": { + "param": { + "type": "string" + }, + "operator": { + "type": "string" + }, + "value": { + "type": ["string", "number", "boolean"] + } + }, + "additionalProperties": true + } + } + }, + "position": { + "type": "string" + }, + "style": { + "type": "string" + }, + "label_placement": { + "type": "string" + }, + "instruction_placement": { + "type": "string" + }, + "hide_on_screen": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": true +} diff --git a/inc/class-scf-json-validator.php b/inc/class-scf-json-validator.php new file mode 100644 index 0000000..16be087 --- /dev/null +++ b/inc/class-scf-json-validator.php @@ -0,0 +1,435 @@ +schema_path = LS_PLUGIN_PLUGIN_DIR . '.github/schemas/scf-field-group.schema.json'; + + if ( class_exists( 'LS_Plugin_SCF_JSON' ) ) { + $this->scf_json = new LS_Plugin_SCF_JSON(); + } + + $this->load_schema(); + } + + /** + * Load JSON schema. + * + * @return bool + */ + private function load_schema() { + if ( ! file_exists( $this->schema_path ) ) { + return false; + } + + $schema_json = file_get_contents( $this->schema_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents + $this->schema = json_decode( $schema_json, true ); + + return null !== $this->schema; + } + + /** + * Validate one field-group JSON file. + * + * @param string $file_path Path to JSON file. + * @return array{valid: bool, errors: array, warnings: array} + */ + public function validate( $file_path ) { + $result = array( + 'valid' => true, + 'errors' => array(), + 'warnings' => array(), + ); + + if ( ! file_exists( $file_path ) ) { + $result['valid'] = false; + $result['errors'][] = 'File does not exist: ' . $file_path; + return $result; + } + + $json_content = file_get_contents( $file_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents + $field_group = json_decode( $json_content, true ); + + if ( JSON_ERROR_NONE !== json_last_error() ) { + $result['valid'] = false; + $result['errors'][] = 'Invalid JSON: ' . json_last_error_msg(); + return $result; + } + + $this->validate_root_structure( $field_group, $result ); + + if ( ! empty( $result['errors'] ) ) { + $result['valid'] = false; + return $result; + } + + $this->validate_field_group_properties( $field_group, $result ); + $this->validate_fields( $field_group, $result ); + $this->validate_location_rules( $field_group, $result ); + $this->validate_display_properties( $field_group, $result ); + + $result['valid'] = empty( $result['errors'] ); + + return $result; + } + + /** + * Validate top-level required structure. + * + * @param array $field_group Field-group data. + * @param array $result Validation result (by reference). + * @return void + */ + private function validate_root_structure( $field_group, &$result ) { + $required = array( 'key', 'title', 'fields', 'location' ); + + foreach ( $required as $property ) { + if ( ! isset( $field_group[ $property ] ) ) { + $result['errors'][] = sprintf( 'Missing required property: %s', $property ); + } + } + + if ( isset( $field_group['key'] ) ) { + if ( ! is_string( $field_group['key'] ) ) { + $result['errors'][] = 'Field group key must be a string'; + } elseif ( 0 !== strpos( $field_group['key'], 'group_' ) ) { + $result['errors'][] = 'Field group key must start with "group_"'; + } + } + + if ( isset( $field_group['title'] ) && ! is_string( $field_group['title'] ) ) { + $result['errors'][] = 'Field group title must be a string'; + } + + if ( isset( $field_group['fields'] ) && ! is_array( $field_group['fields'] ) ) { + $result['errors'][] = 'Fields must be an array'; + } + + if ( isset( $field_group['location'] ) && ! is_array( $field_group['location'] ) ) { + $result['errors'][] = 'Location must be an array'; + } + } + + /** + * Validate field-group display properties. + * + * @param array $field_group Field-group data. + * @param array $result Validation result (by reference). + * @return void + */ + private function validate_field_group_properties( $field_group, &$result ) { + $valid_positions = array( 'acf_after_title', 'normal', 'side' ); + if ( isset( $field_group['position'] ) && ! in_array( $field_group['position'], $valid_positions, true ) ) { + $result['errors'][] = sprintf( + 'Invalid position "%s". Must be one of: %s', + $field_group['position'], + implode( ', ', $valid_positions ) + ); + } + + $valid_styles = array( 'default', 'seamless' ); + if ( isset( $field_group['style'] ) && ! in_array( $field_group['style'], $valid_styles, true ) ) { + $result['warnings'][] = sprintf( + 'Invalid style "%s". Should be one of: %s', + $field_group['style'], + implode( ', ', $valid_styles ) + ); + } + + $valid_label_placements = array( 'top', 'left' ); + if ( isset( $field_group['label_placement'] ) && ! in_array( $field_group['label_placement'], $valid_label_placements, true ) ) { + $result['warnings'][] = 'Invalid label_placement value'; + } + + $valid_instruction_placements = array( 'label', 'field' ); + if ( isset( $field_group['instruction_placement'] ) && ! in_array( $field_group['instruction_placement'], $valid_instruction_placements, true ) ) { + $result['warnings'][] = 'Invalid instruction_placement value'; + } + } + + /** + * Validate all fields. + * + * @param array $field_group Field-group data. + * @param array $result Validation result (by reference). + * @return void + */ + private function validate_fields( $field_group, &$result ) { + if ( ! isset( $field_group['fields'] ) || ! is_array( $field_group['fields'] ) ) { + return; + } + + if ( empty( $field_group['fields'] ) ) { + $result['warnings'][] = 'Field group has no fields defined'; + return; + } + + foreach ( $field_group['fields'] as $index => $field ) { + $this->validate_field( $field, $index, $result ); + } + } + + /** + * Validate an individual field. + * + * @param array $field Field config. + * @param int $index Field index. + * @param array $result Validation result (by reference). + * @return void + */ + private function validate_field( $field, $index, &$result ) { + if ( ! isset( $field['key'] ) ) { + $result['errors'][] = sprintf( 'Field at index %d missing required property: key', $index ); + return; + } + + if ( ! isset( $field['type'] ) ) { + $result['errors'][] = sprintf( 'Field "%s" missing required property: type', $field['key'] ); + return; + } + + if ( ! in_array( $field['type'], $this->valid_field_types, true ) ) { + $result['errors'][] = sprintf( + 'Invalid field type "%s" in field "%s"', + $field['type'], + $field['key'] + ); + } + + $layout_types = array( 'tab', 'accordion', 'message' ); + if ( ! in_array( $field['type'], $layout_types, true ) ) { + if ( 0 !== strpos( $field['key'], 'field_' ) ) { + $result['errors'][] = sprintf( + 'Field key "%s" must start with "field_"', + $field['key'] + ); + } + + if ( ! isset( $field['name'] ) || empty( $field['name'] ) ) { + $result['warnings'][] = sprintf( + 'Field "%s" should have a name for database storage', + $field['key'] + ); + } + } + + $container_types = array( 'group', 'repeater', 'flexible_content' ); + if ( in_array( $field['type'], $container_types, true ) ) { + if ( ! isset( $field['sub_fields'] ) || ! is_array( $field['sub_fields'] ) ) { + $result['errors'][] = sprintf( + 'Field "%s" of type "%s" must have sub_fields array', + $field['key'], + $field['type'] + ); + } + } + + if ( 'flexible_content' === $field['type'] ) { + if ( ! isset( $field['layouts'] ) || ! is_array( $field['layouts'] ) ) { + $result['errors'][] = sprintf( + 'Flexible content field "%s" must have layouts array', + $field['key'] + ); + } + } + } + + /** + * Validate location rule groups. + * + * @param array $field_group Field-group data. + * @param array $result Validation result (by reference). + * @return void + */ + private function validate_location_rules( $field_group, &$result ) { + if ( ! isset( $field_group['location'] ) || ! is_array( $field_group['location'] ) ) { + return; + } + + if ( empty( $field_group['location'] ) ) { + $result['warnings'][] = 'Field group has no location rules defined'; + return; + } + + foreach ( $field_group['location'] as $group_index => $group ) { + if ( ! is_array( $group ) ) { + $result['errors'][] = sprintf( 'Location group %d must be an array', $group_index ); + continue; + } + + foreach ( $group as $rule_index => $rule ) { + $this->validate_location_rule( $rule, $group_index, $rule_index, $result ); + } + } + } + + /** + * Validate one location rule. + * + * @param array $rule Rule config. + * @param int $group_index Group index. + * @param int $rule_index Rule index. + * @param array $result Validation result (by reference). + * @return void + */ + private function validate_location_rule( $rule, $group_index, $rule_index, &$result ) { + if ( ! isset( $rule['param'] ) ) { + $result['errors'][] = sprintf( 'Location rule [%d][%d] missing param', $group_index, $rule_index ); + return; + } + + if ( ! in_array( $rule['param'], $this->valid_location_params, true ) ) { + $result['warnings'][] = sprintf( + 'Location param "%s" may not be valid in [%d][%d]', + $rule['param'], + $group_index, + $rule_index + ); + } + + if ( ! isset( $rule['operator'] ) ) { + $result['errors'][] = sprintf( 'Location rule [%d][%d] missing operator', $group_index, $rule_index ); + } + + if ( ! isset( $rule['value'] ) ) { + $result['errors'][] = sprintf( 'Location rule [%d][%d] missing value', $group_index, $rule_index ); + } + } + + /** + * Validate selected display properties. + * + * @param array $field_group Field-group data. + * @param array $result Validation result (by reference). + * @return void + */ + private function validate_display_properties( $field_group, &$result ) { + if ( isset( $field_group['hide_on_screen'] ) && is_array( $field_group['hide_on_screen'] ) ) { + $valid_hide_values = array( + 'permalink', 'the_content', 'excerpt', 'discussion', 'comments', + 'revisions', 'slug', 'author', 'format', 'page_attributes', + 'featured_image', 'categories', 'tags', 'send-trackbacks', + ); + + foreach ( $field_group['hide_on_screen'] as $hide_value ) { + if ( ! in_array( $hide_value, $valid_hide_values, true ) ) { + $result['warnings'][] = sprintf( 'Unknown hide_on_screen value: %s', $hide_value ); + } + } + } + } + + /** + * Validate all JSON files from plugin SCF directory. + * + * @return array + */ + public function validate_all_files() { + $results = array(); + + if ( ! $this->scf_json ) { + return $results; + } + + $files = $this->scf_json->get_json_files(); + + foreach ( $files as $file_path ) { + $filename = basename( $file_path ); + $results[ $filename ] = $this->validate( $file_path ); + } + + return $results; + } + + /** + * Get loaded schema. + * + * @return array|null + */ + public function get_schema() { + return $this->schema; + } + + /** + * Whether schema file loaded successfully. + * + * @return bool + */ + public function has_schema() { + return ! empty( $this->schema ); + } +} diff --git a/inc/class-scf-json.php b/inc/class-scf-json.php new file mode 100644 index 0000000..702bda7 --- /dev/null +++ b/inc/class-scf-json.php @@ -0,0 +1,228 @@ +json_path = LS_PLUGIN_PLUGIN_DIR . 'scf-json'; + + add_filter( 'acf/settings/save_json', array( $this, 'set_save_path' ) ); + add_filter( 'acf/settings/load_json', array( $this, 'add_load_path' ) ); + + add_filter( 'acf/settings/save_json/type=acf-post-type', array( $this, 'set_save_path' ) ); + add_filter( 'acf/settings/save_json/type=acf-taxonomy', array( $this, 'set_save_path' ) ); + add_filter( 'acf/json/load_paths', array( $this, 'add_post_type_load_paths' ) ); + add_filter( 'acf/json/load_paths', array( $this, 'add_taxonomy_load_paths' ) ); + + $this->maybe_create_directory(); + } + + /** + * Set the save path for JSON exports. + * + * @param string $path Default save path. + * @return string + */ + public function set_save_path( $path ) { + return $this->json_path; + } + + /** + * Add custom load path for field groups. + * + * @param array $paths Existing load paths. + * @return array + */ + public function add_load_path( $paths ) { + return $this->add_unique_load_path( $paths ); + } + + /** + * Add custom load path for post types. + * + * @param array $paths Existing load paths. + * @return array + */ + public function add_post_type_load_paths( $paths ) { + return $this->add_unique_load_path( $paths ); + } + + /** + * Add custom load path for taxonomies. + * + * @param array $paths Existing load paths. + * @return array + */ + public function add_taxonomy_load_paths( $paths ) { + return $this->add_unique_load_path( $paths ); + } + + /** + * Add the plugin path once to load paths. + * + * @param array $paths Existing load paths. + * @return array + */ + private function add_unique_load_path( $paths ) { + if ( ! is_array( $paths ) ) { + $paths = array(); + } + + if ( ! in_array( $this->json_path, $paths, true ) ) { + $paths[] = $this->json_path; + } + + return $paths; + } + + /** + * Create JSON directory when missing. + * + * @return bool + */ + private function maybe_create_directory() { + if ( ! is_dir( $this->json_path ) ) { + return wp_mkdir_p( $this->json_path ); + } + + return true; + } + + /** + * Get JSON directory path. + * + * @return string + */ + public function get_json_path() { + return $this->json_path; + } + + /** + * Get all JSON files in the directory. + * + * @return array + */ + public function get_json_files() { + $files = glob( $this->json_path . '/*.json' ); + + return $files ? $files : array(); + } + + /** + * Basic JSON validation for one file. + * + * @param string $file_path Path to JSON file. + * @return array{valid: bool, errors: array} + */ + public function validate_json_file( $file_path ) { + $result = array( + 'valid' => true, + 'errors' => array(), + ); + + if ( ! file_exists( $file_path ) ) { + $result['valid'] = false; + $result['errors'][] = 'File does not exist.'; + return $result; + } + + $json_content = file_get_contents( $file_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents + $data = json_decode( $json_content, true ); + + if ( JSON_ERROR_NONE !== json_last_error() ) { + $result['valid'] = false; + $result['errors'][] = 'Invalid JSON: ' . json_last_error_msg(); + return $result; + } + + $required = array( 'key', 'title', 'fields', 'location' ); + foreach ( $required as $property ) { + if ( ! isset( $data[ $property ] ) ) { + $result['valid'] = false; + $result['errors'][] = sprintf( 'Missing required property: %s', $property ); + } + } + + if ( isset( $data['key'] ) && 0 !== strpos( $data['key'], 'group_' ) ) { + $result['valid'] = false; + $result['errors'][] = 'Field group key must start with "group_".'; + } + + if ( isset( $data['fields'] ) && ! is_array( $data['fields'] ) ) { + $result['valid'] = false; + $result['errors'][] = 'Fields must be an array.'; + } + + if ( isset( $data['fields'] ) && is_array( $data['fields'] ) ) { + foreach ( $data['fields'] as $index => $field ) { + $field_required = array( 'key', 'type' ); + foreach ( $field_required as $prop ) { + if ( ! isset( $field[ $prop ] ) ) { + $result['valid'] = false; + $result['errors'][] = sprintf( + 'Field at index %d is missing required property: %s', + $index, + $prop + ); + } + } + + $layout_types = array( 'tab', 'accordion', 'message' ); + if ( isset( $field['key'] ) && ! in_array( $field['type'], $layout_types, true ) ) { + if ( 0 !== strpos( $field['key'], 'field_' ) ) { + $result['valid'] = false; + $result['errors'][] = sprintf( + 'Field "%s" key must start with "field_".', + isset( $field['label'] ) ? $field['label'] : $field['key'] + ); + } + } + } + } + + return $result; + } + + /** + * Validate all JSON files in the directory. + * + * @return array + */ + public function validate_all_json_files() { + $results = array(); + $files = $this->get_json_files(); + + foreach ( $files as $file ) { + $filename = basename( $file ); + $results[ $filename ] = $this->validate_json_file( $file ); + } + + return $results; + } +} diff --git a/ls-plugin.php b/ls-plugin.php index 808aed2..a1a7337 100644 --- a/ls-plugin.php +++ b/ls-plugin.php @@ -46,12 +46,32 @@ function ls_plugin_load_textdomain() { function ls_plugin_init() { require_once LS_PLUGIN_PLUGIN_DIR . 'inc/linkable-blocks.php'; require_once LS_PLUGIN_PLUGIN_DIR . 'inc/class-style-switcher.php'; + require_once LS_PLUGIN_PLUGIN_DIR . 'inc/class-scf-json.php'; + require_once LS_PLUGIN_PLUGIN_DIR . 'inc/class-scf-json-validator.php'; $style_switcher = new LS_Plugin_Style_Switcher(); $style_switcher->register_hooks(); + + // Configure SCF to use plugin-managed Local JSON paths. + new LS_Plugin_SCF_JSON(); } add_action( 'plugins_loaded', 'ls_plugin_init' ); +/** + * Returns a shared SCF JSON validator instance. + * + * @return LS_Plugin_SCF_JSON_Validator + */ +function ls_plugin_get_scf_json_validator() { + static $validator = null; + + if ( null === $validator ) { + $validator = new LS_Plugin_SCF_JSON_Validator(); + } + + return $validator; +} + /** * Enqueues Button Icon editor assets. * diff --git a/scf-json/.gitkeep b/scf-json/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scf-json/group_ls_plugin_example.json b/scf-json/group_ls_plugin_example.json new file mode 100644 index 0000000..0e6777b --- /dev/null +++ b/scf-json/group_ls_plugin_example.json @@ -0,0 +1,27 @@ +{ + "key": "group_ls_plugin_example", + "title": "LS Plugin Example Group", + "fields": [ + { + "key": "field_ls_plugin_example_text", + "label": "Example Text", + "name": "ls_plugin_example_text", + "type": "text" + } + ], + "location": [ + [ + { + "param": "post_type", + "operator": "==", + "value": "post" + } + ] + ], + "position": "normal", + "style": "default", + "label_placement": "top", + "instruction_placement": "label", + "active": true, + "description": "Example SCF Local JSON group for ls-plugin." +} From 32007bd3897ac947523621421bc861210586b076 Mon Sep 17 00:00:00 2001 From: Warwick Date: Tue, 14 Apr 2026 17:26:46 +0200 Subject: [PATCH 4/6] feat: Remove deprecated SCF JSON files and add new portfolio fields and taxonomy configurations --- scf-json/.gitkeep | 0 scf-json/group_ls_plugin_example.json | 27 ------ .../group_ls_plugin_portfolio_fields.json | 66 ++++++++++++++ scf-json/post_type_portfolio.json | 86 +++++++++++++++++++ scf-json/taxonomy-portfolio-industry.json | 70 +++++++++++++++ scf-json/taxonomy-portfolio-service.json | 68 +++++++++++++++ 6 files changed, 290 insertions(+), 27 deletions(-) delete mode 100644 scf-json/.gitkeep delete mode 100644 scf-json/group_ls_plugin_example.json create mode 100644 scf-json/group_ls_plugin_portfolio_fields.json create mode 100644 scf-json/post_type_portfolio.json create mode 100644 scf-json/taxonomy-portfolio-industry.json create mode 100644 scf-json/taxonomy-portfolio-service.json diff --git a/scf-json/.gitkeep b/scf-json/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/scf-json/group_ls_plugin_example.json b/scf-json/group_ls_plugin_example.json deleted file mode 100644 index 0e6777b..0000000 --- a/scf-json/group_ls_plugin_example.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "key": "group_ls_plugin_example", - "title": "LS Plugin Example Group", - "fields": [ - { - "key": "field_ls_plugin_example_text", - "label": "Example Text", - "name": "ls_plugin_example_text", - "type": "text" - } - ], - "location": [ - [ - { - "param": "post_type", - "operator": "==", - "value": "post" - } - ] - ], - "position": "normal", - "style": "default", - "label_placement": "top", - "instruction_placement": "label", - "active": true, - "description": "Example SCF Local JSON group for ls-plugin." -} diff --git a/scf-json/group_ls_plugin_portfolio_fields.json b/scf-json/group_ls_plugin_portfolio_fields.json new file mode 100644 index 0000000..7f8e54e --- /dev/null +++ b/scf-json/group_ls_plugin_portfolio_fields.json @@ -0,0 +1,66 @@ +{ + "key": "group_ls_plugin_portfolio_fields", + "title": "Portfolio Fields", + "fields": [ + { + "key": "field_ls_plugin_portfolio_logo", + "label": "Portfolio Logo", + "name": "ls_plugin_porfolio_logo", + "aria-label": "", + "type": "image", + "instructions": "", + "required": 0, + "conditional_logic": 0, + "wrapper": { + "width": "", + "class": "", + "id": "" + }, + "return_format": "array", + "library": "all", + "min_width": "", + "min_height": "", + "min_size": "", + "max_width": "", + "max_height": "", + "max_size": "", + "mime_types": "", + "preview_size": "medium" + }, + { + "key": "field_ls_plugin_portfolio_website", + "label": "Portfolio Website", + "name": "ls_plugin_portfolio_website", + "aria-label": "", + "type": "url", + "instructions": "", + "required": 0, + "conditional_logic": 0, + "wrapper": { + "width": "", + "class": "", + "id": "" + }, + "placeholder": "https://", + "default_value": "" + } + ], + "location": [ + [ + { + "param": "post_type", + "operator": "==", + "value": "ls_plugin_portfolio" + } + ] + ], + "menu_order": 0, + "position": "normal", + "style": "default", + "label_placement": "top", + "instruction_placement": "label", + "hide_on_screen": [], + "active": true, + "description": "", + "show_in_rest": 1 +} diff --git a/scf-json/post_type_portfolio.json b/scf-json/post_type_portfolio.json new file mode 100644 index 0000000..925dc00 --- /dev/null +++ b/scf-json/post_type_portfolio.json @@ -0,0 +1,86 @@ +{ + "key": "post_type_ls_plugin_portfolio", + "title": "Portfolios", + "menu_order": 0, + "active": true, + "post_type": "ls_plugin_portfolio", + "advanced_configuration": true, + "labels": { + "name": "Portfolios", + "singular_name": "Portfolio", + "menu_name": "Portfolios", + "all_items": "All Portfolios", + "edit_item": "Edit Portfolio", + "view_item": "View Portfolio", + "view_items": "View Portfolios", + "add_new_item": "Add New Portfolio", + "add_new": "Add New", + "new_item": "New Portfolio", + "parent_item_colon": "Parent Portfolio:", + "search_items": "Search Portfolios", + "not_found": "No portfolios found", + "not_found_in_trash": "No portfolios found in Trash", + "archives": "Portfolio Archives", + "attributes": "Portfolio Attributes", + "insert_into_item": "Insert into portfolio", + "uploaded_to_this_item": "Uploaded to this portfolio", + "filter_items_list": "Filter portfolios list", + "filter_by_date": "Filter portfolios by date", + "items_list_navigation": "Portfolios list navigation", + "items_list": "Portfolios list", + "item_published": "Portfolio published.", + "item_published_privately": "Portfolio published privately.", + "item_reverted_to_draft": "Portfolio reverted to draft.", + "item_scheduled": "Portfolio scheduled.", + "item_updated": "Portfolio updated.", + "item_link": "Portfolio Link", + "item_link_description": "A link to a portfolio." + }, + "description": "", + "public": true, + "hierarchical": false, + "exclude_from_search": false, + "publicly_queryable": true, + "show_ui": true, + "show_in_menu": true, + "admin_menu_parent": "", + "show_in_admin_bar": true, + "show_in_nav_menus": true, + "show_in_rest": true, + "rest_base": "", + "rest_namespace": "wp/v2", + "rest_controller_class": "WP_REST_Posts_Controller", + "menu_position": "", + "menu_icon": { + "type": "dashicons", + "value": "dashicons-portfolio" + }, + "rename_capabilities": false, + "singular_capability_name": "post", + "plural_capability_name": "posts", + "supports": [ + "title", + "editor", + "excerpt", + "thumbnail", + "custom-fields" + ], + "taxonomies": [ + "ls_plugin_portfolio_industry", + "ls_plugin_portfolio_service" + ], + "has_archive": true, + "has_archive_slug": "portfolio", + "rewrite": { + "permalink_rewrite": "custom_permalink", + "slug": "portfolio", + "with_front": "1", + "feeds": "1", + "pages": "1" + }, + "query_var": "post_type_key", + "query_var_name": "", + "can_export": true, + "delete_with_user": false, + "enter_title_here": "" +} diff --git a/scf-json/taxonomy-portfolio-industry.json b/scf-json/taxonomy-portfolio-industry.json new file mode 100644 index 0000000..31a7438 --- /dev/null +++ b/scf-json/taxonomy-portfolio-industry.json @@ -0,0 +1,70 @@ +{ + "key": "taxonomy_ls_plugin_portfolio_industry", + "title": "Industry", + "menu_order": 0, + "active": true, + "taxonomy": "ls_plugin_portfolio_industry", + "object_type": [ + "ls_plugin_portfolio" + ], + "advanced_configuration": true, + "labels": { + "name": "Industries", + "singular_name": "Industry", + "menu_name": "Industries", + "all_items": "All Industries", + "edit_item": "Edit Industry", + "view_item": "View Industry", + "update_item": "Update Industry", + "add_new_item": "Add New Industry", + "new_item_name": "New Industry Name", + "parent_item": "Parent Industry", + "parent_item_colon": "Parent Industry:", + "search_items": "Search Industries", + "popular_items": "Popular Industries", + "separate_items_with_commas": "Separate industries with commas", + "add_or_remove_items": "Add or remove industries", + "choose_from_most_used": "Choose from the most used industries", + "not_found": "No industries found", + "items_list_navigation": "Industries list navigation", + "items_list": "Industries list", + "back_to_items": "Back to Industries", + "item_link": "Industry Link", + "item_link_description": "A link to an industry." + }, + "description": "", + "capabilities": { + "manage_terms": "manage_categories", + "edit_terms": "manage_categories", + "delete_terms": "manage_categories", + "assign_terms": "edit_posts" + }, + "public": true, + "publicly_queryable": true, + "hierarchical": true, + "show_ui": true, + "show_in_menu": true, + "show_in_nav_menus": true, + "show_in_rest": true, + "rest_base": "portfolio-industry", + "rest_namespace": "wp/v2", + "rest_controller_class": "", + "show_tagcloud": true, + "show_in_quick_edit": true, + "show_admin_column": true, + "rewrite": { + "slug": "portfolio-industry", + "permalink_rewrite": "custom_permalink", + "with_front": "1", + "rewrite_hierarchical": "1" + }, + "query_var": "post_type_key", + "query_var_name": "", + "default_term": { + "default_term_enabled": "0" + }, + "sort": false, + "meta_box": "default", + "meta_box_cb": "", + "meta_box_sanitize_cb": "" +} diff --git a/scf-json/taxonomy-portfolio-service.json b/scf-json/taxonomy-portfolio-service.json new file mode 100644 index 0000000..2a08fa8 --- /dev/null +++ b/scf-json/taxonomy-portfolio-service.json @@ -0,0 +1,68 @@ +{ + "key": "taxonomy_ls_plugin_portfolio_service", + "title": "Service", + "menu_order": 0, + "active": true, + "taxonomy": "ls_plugin_portfolio_service", + "object_type": [ + "ls_plugin_portfolio" + ], + "advanced_configuration": true, + "labels": { + "name": "Services", + "singular_name": "Service", + "menu_name": "Services", + "all_items": "All Services", + "edit_item": "Edit Service", + "view_item": "View Service", + "update_item": "Update Service", + "add_new_item": "Add New Service", + "new_item_name": "New Service Name", + "search_items": "Search Services", + "popular_items": "Popular Services", + "separate_items_with_commas": "Separate services with commas", + "add_or_remove_items": "Add or remove services", + "choose_from_most_used": "Choose from the most used services", + "not_found": "No services found", + "items_list_navigation": "Services list navigation", + "items_list": "Services list", + "back_to_items": "Back to Services", + "item_link": "Service Link", + "item_link_description": "A link to a service." + }, + "description": "", + "capabilities": { + "manage_terms": "manage_categories", + "edit_terms": "manage_categories", + "delete_terms": "manage_categories", + "assign_terms": "edit_posts" + }, + "public": true, + "publicly_queryable": true, + "hierarchical": false, + "show_ui": true, + "show_in_menu": true, + "show_in_nav_menus": true, + "show_in_rest": true, + "rest_base": "portfolio-service", + "rest_namespace": "wp/v2", + "rest_controller_class": "", + "show_tagcloud": true, + "show_in_quick_edit": true, + "show_admin_column": true, + "rewrite": { + "slug": "portfolio-service", + "permalink_rewrite": "custom_permalink", + "with_front": "1", + "rewrite_hierarchical": "0" + }, + "query_var": "post_type_key", + "query_var_name": "", + "default_term": { + "default_term_enabled": "0" + }, + "sort": false, + "meta_box": "default", + "meta_box_cb": "", + "meta_box_sanitize_cb": "" +} From e943cbc8a0a1274680566815081d4e1001a3d5d8 Mon Sep 17 00:00:00 2001 From: Warwick Date: Wed, 15 Apr 2026 13:56:42 +0200 Subject: [PATCH 5/6] feat: add SCF permalink controls --- CHANGELOG.md | 7 +- inc/class-permalinks.php | 278 +++++++++++++++++++++++++++++++++++++++ ls-plugin.php | 4 + 3 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 inc/class-permalinks.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 0485734..eca0453 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,13 +14,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added a Button Icon selector panel for core Button blocks, including left/right positioning and up/down icon options. - Added a Back to Top option as a `core/button` variation so users inherit native Button styling controls and icon compatibility. - Added smooth scrolling support for Back to Top button clicks and internal anchor links using vanilla JavaScript. - - Linkable Group Blocks support for `core/group`, `core/column`, and `core/cover`, including custom URLs and current-post linking from the block toolbar. +- Added plugin-managed SCF Local JSON handling and validation utilities for field groups, post types, and taxonomies. +- Added an SCF field-group schema for repository validation workflows. +- Added SCF JSON definitions for a Portfolio post type, Industry and Service taxonomies, and Portfolio custom fields. +- Added SCF permalink controls on the WordPress Permalinks screen for portfolio archives and related taxonomies. ### Changed - Changed Back to Top implementation from a standalone custom block to a `core/button` variation. - Changed Back to Top frontend targeting to use a dedicated wrapper class (`is-back-to-top`) for reliable JS and CSS behaviour. - Changed Back to Top visibility to always display (removed scroll-threshold hide/show behaviour). +- Changed linkable block source assets to load from `src/` and updated build configuration to match the new asset layout. +- Changed the plugin bootstrap to load SCF JSON configuration, validation, and permalink management classes. ### Deprecated diff --git a/inc/class-permalinks.php b/inc/class-permalinks.php new file mode 100644 index 0000000..ed3e8a0 --- /dev/null +++ b/inc/class-permalinks.php @@ -0,0 +1,278 @@ + 'portfolio', + 'portfolio-industry' => 'portfolio-industry', + 'portfolio-service' => 'portfolio-service', + ); + + /** + * Constructor. + */ + public function __construct() { + add_action( 'admin_init', array( $this, 'register_permalink_settings' ) ); + add_action( 'admin_init', array( $this, 'save_custom_permalink_fields' ), 20 ); + add_filter( 'acf/post_type/registration_args', array( $this, 'apply_post_type_slugs' ), 10, 2 ); + add_filter( 'acf/taxonomy/registration_args', array( $this, 'apply_taxonomy_slugs' ), 10, 2 ); + } + + /** + * Register the setting to save custom fields. + */ + public function register_permalink_settings() { + register_setting( + 'permalink', + 'ls_plugin_scf_slugs', + array( + 'type' => 'array', + 'sanitize_callback' => array( $this, 'sanitize_permalink_fields' ), + 'default' => $this->defaults, + ) + ); + + add_settings_section( + 'ls_plugin_scf_permalink_section', + '', + array( $this, 'permalink_fields' ), + 'permalink' + ); + } + + /** + * Sanitize the custom permalink fields before saving. + * + * @param array $input Raw input from the form. + * @return array Sanitized input. + */ + public function sanitize_permalink_fields( $input ) { + $sanitized = array(); + + foreach ( $this->defaults as $key => $default ) { + $field_key = 'ls_plugin_scf_' . $key; + $sanitized[ $field_key ] = isset( $input[ $field_key ] ) ? sanitize_text_field( $input[ $field_key ] ) : ''; + } + + return $sanitized; + } + + /** + * Register new fields to the permalink settings page. + */ + public function permalink_fields() { + // Get existing options or defaults. + $options = get_option( 'ls_plugin_scf_slugs', $this->defaults ); + + $post_type_fields = array( + 'portfolio' => array( + 'label' => esc_html__( 'Portfolio', 'ls-plugin' ), + 'description' => esc_html__( 'Single Portfolio Archive', 'ls-plugin' ), + ), + ); + + $taxonomy_fields = array( + 'portfolio-industry' => array( + 'label' => esc_html__( 'Industry', 'ls-plugin' ), + 'description' => esc_html__( 'Portfolio Industry Taxonomy', 'ls-plugin' ), + ), + 'portfolio-service' => array( + 'label' => esc_html__( 'Service', 'ls-plugin' ), + 'description' => esc_html__( 'Portfolio Service Taxonomy', 'ls-plugin' ), + ), + ); + ?> +

+

+ +

+ + $field ) { ?> + defaults[ $key ]; + ?> + + + + + +
+ + + +

+ +

+
+ +

+ + $field ) { ?> + defaults[ $key ]; + ?> + + + + + +
+ + + +

+ +

+
+ sanitize_permalink_fields( $input ); + update_option( 'ls_plugin_scf_slugs', $sanitized ); + } + } + + /** + * Apply custom post type slugs from saved options. + * + * Filters SCF post type registration arguments to apply custom slugs. + * This hook is called by ACF/SCF before register_post_type(). + * + * @param array $args Post type registration arguments. + * @param array $post SCF post type configuration. + * @return array Modified post type registration arguments. + */ + public function apply_post_type_slugs( $args, $post ) { + $slug_options = get_option( 'ls_plugin_scf_slugs', $this->defaults ); + + // Check if this is a post type we manage. + $post_type_slug = $post['post_type'] ?? ''; + $has_archive = $args['has_archive'] ?? false; + + if ( ! $has_archive ) { + return $args; + } + + if ( 'ls_plugin_portfolio' === $post_type_slug ) { + $field_key = 'ls_plugin_scf_portfolio'; + $custom_slug = isset( $slug_options[ $field_key ] ) ? $slug_options[ $field_key ] : ''; + + if ( '' !== $custom_slug ) { + $args['rewrite'] = $args['rewrite'] ?? array(); + $args['rewrite']['slug'] = $custom_slug; + $args['has_archive'] = $custom_slug; + } + } + + return $args; + } + + /** + * Apply custom taxonomy slugs from saved options. + * + * Filters SCF taxonomy registration arguments to apply custom slugs. + * This hook is called by ACF/SCF before register_taxonomy(). + * + * @param array $args Taxonomy registration arguments. + * @param array $post SCF taxonomy configuration. + * @return array Modified taxonomy registration arguments. + */ + public function apply_taxonomy_slugs( $args, $post ) { + $slug_options = get_option( 'ls_plugin_scf_slugs', $this->defaults ); + + $taxonomy_slug = $post['taxonomy'] ?? ''; + + // Map taxonomy slugs to custom configuration keys. + $taxonomy_mapping = array( + 'ls_plugin_portfolio_industry' => 'portfolio-industry', + 'ls_plugin_portfolio_service' => 'portfolio-service', + ); + + if ( ! isset( $taxonomy_mapping[ $taxonomy_slug ] ) ) { + return $args; + } + + $config_key = $taxonomy_mapping[ $taxonomy_slug ]; + $field_key = 'ls_plugin_scf_' . $config_key; + $custom_slug = isset( $slug_options[ $field_key ] ) ? $slug_options[ $field_key ] : ''; + + if ( '' !== $custom_slug ) { + $args['rewrite'] = $args['rewrite'] ?? array(); + $args['rewrite']['slug'] = $custom_slug; + } + + return $args; + } +} diff --git a/ls-plugin.php b/ls-plugin.php index a1a7337..9c40f01 100644 --- a/ls-plugin.php +++ b/ls-plugin.php @@ -48,12 +48,16 @@ function ls_plugin_init() { require_once LS_PLUGIN_PLUGIN_DIR . 'inc/class-style-switcher.php'; require_once LS_PLUGIN_PLUGIN_DIR . 'inc/class-scf-json.php'; require_once LS_PLUGIN_PLUGIN_DIR . 'inc/class-scf-json-validator.php'; + require_once LS_PLUGIN_PLUGIN_DIR . 'inc/class-permalinks.php'; $style_switcher = new LS_Plugin_Style_Switcher(); $style_switcher->register_hooks(); // Configure SCF to use plugin-managed Local JSON paths. new LS_Plugin_SCF_JSON(); + + // Manage custom permalinks for SCF post types and taxonomies. + new LS_Plugin\Permalinks(); } add_action( 'plugins_loaded', 'ls_plugin_init' ); From 42173c8b168929a47dfa83fe7b9f443aa1a9b7bf Mon Sep 17 00:00:00 2001 From: Warwick Date: Wed, 15 Apr 2026 15:16:59 +0200 Subject: [PATCH 6/6] fix: correct typo in portfolio logo field name and update permalink sanitization method --- inc/class-permalinks.php | 2 +- scf-json/group_ls_plugin_portfolio_fields.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/inc/class-permalinks.php b/inc/class-permalinks.php index ed3e8a0..e49a676 100644 --- a/inc/class-permalinks.php +++ b/inc/class-permalinks.php @@ -77,7 +77,7 @@ public function sanitize_permalink_fields( $input ) { foreach ( $this->defaults as $key => $default ) { $field_key = 'ls_plugin_scf_' . $key; - $sanitized[ $field_key ] = isset( $input[ $field_key ] ) ? sanitize_text_field( $input[ $field_key ] ) : ''; + $sanitized[ $field_key ] = isset( $input[ $field_key ] ) ? sanitize_title( $input[ $field_key ] ) : ''; } return $sanitized; diff --git a/scf-json/group_ls_plugin_portfolio_fields.json b/scf-json/group_ls_plugin_portfolio_fields.json index 7f8e54e..15a1e9f 100644 --- a/scf-json/group_ls_plugin_portfolio_fields.json +++ b/scf-json/group_ls_plugin_portfolio_fields.json @@ -5,7 +5,7 @@ { "key": "field_ls_plugin_portfolio_logo", "label": "Portfolio Logo", - "name": "ls_plugin_porfolio_logo", + "name": "ls_plugin_portfolio_logo", "aria-label": "", "type": "image", "instructions": "",