@@ -1,95 +1,45 @@
|
|||||||
# WP Plugin Auto-Release (Gitea Actions)
|
# Advanced SEO Meta + AI FAQ Schema
|
||||||
|
|
||||||
Automatically versions and releases this WordPress plugin whenever the
|
Advanced SEO Meta + AI FAQ Schema is a WordPress plugin that helps you control how your content appears in search results and AI-powered search experiences.
|
||||||
plugin's `Version:` header changes on `main`. No manual tagging, no
|
|
||||||
hardcoded plugin name/slug — everything is read straight from the plugin's
|
|
||||||
own file header.
|
|
||||||
|
|
||||||
## What it does
|
## Features
|
||||||
|
|
||||||
On every push to `main`, the workflow at
|
- Custom SEO title field for posts and pages
|
||||||
[`.gitea/workflows/wp-plugin-release.yml`](.gitea/workflows/wp-plugin-release.yml) runs:
|
- Custom meta description field for better search snippet control
|
||||||
|
- FAQ builder that lets you add multiple questions and answers
|
||||||
|
- Automatic JSON-LD FAQPage schema output for AI search engines such as Google SGE and Bing Chat
|
||||||
|
- Open Graph tags for Facebook and LinkedIn sharing
|
||||||
|
- Clean meta box in the WordPress editor for easy content optimization
|
||||||
|
|
||||||
1. **Locate the main plugin file** — scans all `*.php` files for one
|
## What the plugin adds
|
||||||
containing a `Plugin Name:` header (skips `vendor/`, `node_modules/`,
|
|
||||||
`build/`).
|
|
||||||
2. **Extract metadata** — reads `Plugin Name:` and `Version:` from that
|
|
||||||
file's header comment, then derives a slug from the plugin name
|
|
||||||
(e.g. `My Cool Plugin` → `my-cool-plugin`).
|
|
||||||
3. **Check for a new version** — looks for a git tag `vX.Y.Z` matching the
|
|
||||||
version found in step 2.
|
|
||||||
- **Tag already exists** → workflow stops here. Pushing commits without
|
|
||||||
bumping the version does nothing.
|
|
||||||
- **Tag doesn't exist** → it's a new release, continue.
|
|
||||||
4. **Sync `readme.txt`** — updates the `Stable tag:` line to match, if the
|
|
||||||
file exists, and commits that change back to `main`.
|
|
||||||
5. **Tag** the commit as `vX.Y.Z` and push the tag.
|
|
||||||
6. **Build a zip** of the plugin (excluding `.git`, `.gitea`, `.github`,
|
|
||||||
`node_modules`, `build`), named `<slug>-<version>.zip`.
|
|
||||||
7. **Publish a Gitea Release** for the new tag with the zip attached as a
|
|
||||||
downloadable asset.
|
|
||||||
|
|
||||||
## Requirements
|
When editing a post or page, you can fill in:
|
||||||
|
|
||||||
- A **Gitea Actions runner** (`act_runner`) registered on your instance.
|
- SEO Title
|
||||||
- The runner image needs `rsync`, `zip`, and `git` available (standard on
|
- Meta Description
|
||||||
`ubuntu-latest`-style runner images).
|
- FAQ questions and answers
|
||||||
- A **repo secret** named `RELEASE_TOKEN` — a Gitea access token with
|
|
||||||
repo write / release permissions. Add it under
|
The plugin then outputs:
|
||||||
**Repo → Settings → Secrets**.
|
|
||||||
|
- A custom page title override
|
||||||
|
- A meta description tag
|
||||||
|
- Open Graph meta tags
|
||||||
|
- Structured FAQ schema in JSON-LD format
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Upload the plugin folder to your WordPress installation's plugins directory.
|
||||||
|
2. Activate the plugin from the WordPress admin panel.
|
||||||
|
3. Edit any post or page and look for the "Advanced SEO & AI FAQ Schema" meta box.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
1. Bump the version in your plugin's main file header:
|
1. Open a post or page in the editor.
|
||||||
|
2. Enter a custom SEO title and meta description.
|
||||||
|
3. Add one or more FAQ entries using the built-in builder.
|
||||||
|
4. Publish or update the post to make the SEO and schema output available on the front end.
|
||||||
|
|
||||||
```php
|
## Notes
|
||||||
/**
|
|
||||||
* Plugin Name: My Cool Plugin
|
|
||||||
* Version: 1.3.0
|
|
||||||
*/
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Commit and push to `main`:
|
The plugin is designed for posts and pages and works best when used alongside strong on-page content. The FAQ schema is especially useful for improving visibility in AI-driven search experiences.
|
||||||
|
|
||||||
```bash
|
|
||||||
git add .
|
|
||||||
git commit -m "Release 1.3.0"
|
|
||||||
git push origin main
|
|
||||||
```
|
|
||||||
|
|
||||||
3. The workflow detects the new version, tags `v1.3.0`, builds
|
|
||||||
`my-cool-plugin-1.3.0.zip`, and publishes it as a Gitea release —
|
|
||||||
nothing else to do.
|
|
||||||
|
|
||||||
## Plugin file requirements
|
|
||||||
|
|
||||||
Your main plugin file must use the standard WordPress header format,
|
|
||||||
with `Plugin Name:` and `Version:` inside the top `/** ... */` comment
|
|
||||||
block:
|
|
||||||
|
|
||||||
```php
|
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Plugin Name: My Cool Plugin
|
|
||||||
* Description: Does cool things.
|
|
||||||
* Version: 1.3.0
|
|
||||||
* Author: You
|
|
||||||
*/
|
|
||||||
```
|
|
||||||
|
|
||||||
If your header format differs significantly (e.g. Version defined only
|
|
||||||
as a PHP constant with no header line), the parsing step in the workflow
|
|
||||||
will need a small adjustment.
|
|
||||||
|
|
||||||
## Notes & caveats
|
|
||||||
|
|
||||||
- **One release per version.** If you push multiple commits without
|
|
||||||
changing `Version:`, no new tag/release is created — this avoids
|
|
||||||
duplicate releases on every push.
|
|
||||||
- **`readme.txt` sync** only runs if the file exists at the repo root;
|
|
||||||
it's skipped otherwise.
|
|
||||||
- **Tags are immutable once created** by this workflow — to "redo" a
|
|
||||||
release, delete the tag and Gitea release first, then push again.
|
|
||||||
- The zip excludes `.git`, `.gitea`, `.github`, `node_modules`, and
|
|
||||||
`build` — add more `--exclude` patterns in the "Build plugin zip" step
|
|
||||||
if you have other dev-only directories (e.g. `tests/`, `.vscode/`).
|
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Advanced SEO Meta + AI FAQ Schema
|
||||||
|
* Plugin URI: https://my-dev.pro/advanced-seo-meta
|
||||||
|
* Description: Full SEO meta box (Title, Description) + built-in FAQ builder with JSON-LD Schema for AI search engines.
|
||||||
|
* Version: 3.0.0
|
||||||
|
* Author: MYDev
|
||||||
|
* Author URI: https://my-dev.pro
|
||||||
|
* License: GPL-2.0+
|
||||||
|
* Text Domain: advanced-seo-meta
|
||||||
|
* Domain Path: /languages
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) exit;
|
||||||
|
|
||||||
|
class Advanced_SEO_Meta {
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
add_action( 'add_meta_boxes', array( $this, 'add_meta_box' ) );
|
||||||
|
add_action( 'save_post', array( $this, 'save_meta_data' ) );
|
||||||
|
add_filter( 'pre_get_document_title', array( $this, 'custom_title' ), 10 );
|
||||||
|
add_action( 'wp_head', array( $this, 'output_meta_tags' ), 1 );
|
||||||
|
add_action( 'wp_head', array( $this, 'output_og_tags' ), 2 );
|
||||||
|
add_action( 'wp_head', array( $this, 'output_faq_schema' ), 5 ); // High priority for AI crawlers
|
||||||
|
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a tiny JS script to handle adding/removing FAQ rows in the admin.
|
||||||
|
*/
|
||||||
|
public function enqueue_admin_assets() {
|
||||||
|
wp_add_inline_script( 'jquery', '
|
||||||
|
jQuery(document).ready(function($) {
|
||||||
|
// Add FAQ row
|
||||||
|
$(document).on("click", ".add-faq-row", function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var index = $(".faq-row").length;
|
||||||
|
var row = \'<div class="faq-row" style="background:#f9f9f9;padding:15px;margin-bottom:15px;border:1px solid #ddd;">\' +
|
||||||
|
\'<p><label>Question:</label><input type="text" name="faq_question[]" style="width:100%;" /></p>\' +
|
||||||
|
\'<p><label>Answer:</label><textarea name="faq_answer[]" rows="2" style="width:100%;"></textarea></p>\' +
|
||||||
|
\'<button class="remove-faq-row button button-small">Remove Question</button>\' +
|
||||||
|
\'</div>\';
|
||||||
|
$(this).before(row);
|
||||||
|
});
|
||||||
|
// Remove FAQ row
|
||||||
|
$(document).on("click", ".remove-faq-row", function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$(this).closest(".faq-row").remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the main SEO + FAQ meta box.
|
||||||
|
*/
|
||||||
|
public function add_meta_box() {
|
||||||
|
add_meta_box(
|
||||||
|
'advanced_seo_meta_box',
|
||||||
|
__( 'Advanced SEO & AI FAQ Schema', 'advanced-seo-meta' ),
|
||||||
|
array( $this, 'render_meta_box' ),
|
||||||
|
['post', 'page'],
|
||||||
|
'normal',
|
||||||
|
'default'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the meta box (Title, Description, FAQ builder).
|
||||||
|
*/
|
||||||
|
public function render_meta_box( $post ) {
|
||||||
|
wp_nonce_field( 'advanced_seo_nonce', 'advanced_seo_nonce' );
|
||||||
|
|
||||||
|
// Get saved values
|
||||||
|
$seo_title = get_post_meta( $post->ID, '_seo_title', true );
|
||||||
|
$seo_description = get_post_meta( $post->ID, '_seo_description', true );
|
||||||
|
$faq_data = get_post_meta( $post->ID, '_seo_faq', true );
|
||||||
|
if ( ! is_array( $faq_data ) ) $faq_data = array();
|
||||||
|
?>
|
||||||
|
<style>
|
||||||
|
.seo-field { margin-bottom: 20px; }
|
||||||
|
.seo-field label { font-weight: 600; display: block; margin-bottom: 5px; }
|
||||||
|
.seo-field input, .seo-field textarea { width: 100%; max-width: 700px; }
|
||||||
|
.faq-row { background: #f5f5f5; padding: 15px; margin-bottom: 15px; border: 1px solid #ddd; border-radius: 4px; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- 1. SEO Title -->
|
||||||
|
<div class="seo-field">
|
||||||
|
<label for="seo_title"><?php esc_html_e( 'SEO Title (For Google search results)', 'advanced-seo-meta' ); ?></label>
|
||||||
|
<input type="text" id="seo_title" name="seo_title" value="<?php echo esc_attr( $seo_title ); ?>" placeholder="e.g., Comprehensive Genomic Profiling (CGP) & WES for Precision Oncology" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. Meta Description -->
|
||||||
|
<div class="seo-field">
|
||||||
|
<label for="seo_description"><?php esc_html_e( 'Meta Description (150-160 characters)', 'advanced-seo-meta' ); ?></label>
|
||||||
|
<textarea id="seo_description" name="seo_description" rows="3"><?php echo esc_textarea( $seo_description ); ?></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr style="margin: 30px 0;" />
|
||||||
|
|
||||||
|
<!-- 3. FAQ Builder (For AI Search Engines) -->
|
||||||
|
<h3 style="margin-top:0;"><?php esc_html_e( 'FAQ Schema for AI Search (Google SGE, Bing Chat)', 'advanced-seo-meta' ); ?></h3>
|
||||||
|
<p class="description"><?php esc_html_e( 'Add questions and answers here. This will be output as JSON-LD structured data so AI engines can display your answers directly in search results.', 'advanced-seo-meta' ); ?></p>
|
||||||
|
|
||||||
|
<div id="faq-container">
|
||||||
|
<?php if ( ! empty( $faq_data ) ) : ?>
|
||||||
|
<?php foreach ( $faq_data as $faq ) : ?>
|
||||||
|
<div class="faq-row">
|
||||||
|
<p><label>Question:</label><input type="text" name="faq_question[]" value="<?php echo esc_attr( $faq['question'] ); ?>" style="width:100%;" /></p>
|
||||||
|
<p><label>Answer:</label><textarea name="faq_answer[]" rows="2" style="width:100%;"><?php echo esc_textarea( $faq['answer'] ); ?></textarea></p>
|
||||||
|
<button class="remove-faq-row button button-small"><?php esc_html_e( 'Remove Question', 'advanced-seo-meta' ); ?></button>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php else : ?>
|
||||||
|
<!-- Default Example Pre-filled (Your Oncology Content) -->
|
||||||
|
<div class="faq-row">
|
||||||
|
<p><label>Question:</label><input type="text" name="faq_question[]" value="What is Comprehensive Genomic Profiling (CGP) in oncology?" style="width:100%;" /></p>
|
||||||
|
<p><label>Answer:</label><textarea name="faq_answer[]" rows="2" style="width:100%;">Comprehensive Genomic Profiling (CGP) is a next-generation sequencing test that analyzes all classes of genomic alterations across hundreds of cancer-associated genes to identify targeted therapy options, immunotherapy biomarkers, and clinical trial opportunities for cancer patients.</textarea></p>
|
||||||
|
<button class="remove-faq-row button button-small">Remove Question</button>
|
||||||
|
</div>
|
||||||
|
<div class="faq-row">
|
||||||
|
<p><label>Question:</label><input type="text" name="faq_question[]" value="What is the difference between WES (Whole Exome Sequencing) and CGP?" style="width:100%;" /></p>
|
||||||
|
<p><label>Answer:</label><textarea name="faq_answer[]" rows="2" style="width:100%;">Whole Exome Sequencing (WES) sequences all protein-coding regions of the entire genome, covering ~20,000 genes. Comprehensive Genomic Profiling (CGP) focuses specifically on a curated panel of known cancer-driver genes, providing deeper coverage (higher read depth) for those specific genes, making it more sensitive for detecting low-frequency tumor mutations in clinical oncology.</textarea></p>
|
||||||
|
<button class="remove-faq-row button button-small">Remove Question</button>
|
||||||
|
</div>
|
||||||
|
<div class="faq-row">
|
||||||
|
<p><label>Question:</label><input type="text" name="faq_question[]" value="How does precision medicine use genomic profiling?" style="width:100%;" /></p>
|
||||||
|
<p><label>Answer:</label><textarea name="faq_answer[]" rows="2" style="width:100%;">Precision medicine uses genomic profiling to identify specific genetic mutations driving a patient's disease. By matching these mutations with FDA-approved targeted therapies or immunotherapies, clinicians can deliver highly personalized treatment plans that improve efficacy and reduce adverse side effects compared to traditional chemotherapy.</textarea></p>
|
||||||
|
<button class="remove-faq-row button button-small">Remove Question</button>
|
||||||
|
</div>
|
||||||
|
<div class="faq-row">
|
||||||
|
<p><label>Question:</label><input type="text" name="faq_question[]" value="Is CGP the same as standard clinical genetics testing?" style="width:100%;" /></p>
|
||||||
|
<p><label>Answer:</label><textarea name="faq_answer[]" rows="2" style="width:100%;">No. Standard clinical genetics testing typically looks for inherited germline mutations (e.g., BRCA1/2). CGP analyzes somatic (tumor-acquired) mutations within the cancer tissue itself. While both fall under clinical genetics, CGP is specifically used for guiding oncology treatment decisions, whereas standard testing assesses hereditary cancer risk.</textarea></p>
|
||||||
|
<button class="remove-faq-row button button-small">Remove Question</button>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="add-faq-row button button-primary"><?php esc_html_e( '+ Add New FAQ', 'advanced-seo-meta' ); ?></button>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save all the new fields.
|
||||||
|
*/
|
||||||
|
public function save_meta_data( $post_id ) {
|
||||||
|
if ( ! isset( $_POST['advanced_seo_nonce'] ) || ! wp_verify_nonce( $_POST['advanced_seo_nonce'], 'advanced_seo_nonce' ) ) return;
|
||||||
|
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;
|
||||||
|
if ( ! current_user_can( 'edit_post', $post_id ) ) return;
|
||||||
|
|
||||||
|
// Save Title & Description
|
||||||
|
if ( isset( $_POST['seo_title'] ) ) update_post_meta( $post_id, '_seo_title', sanitize_text_field( $_POST['seo_title'] ) );
|
||||||
|
if ( isset( $_POST['seo_description'] ) ) update_post_meta( $post_id, '_seo_description', sanitize_textarea_field( $_POST['seo_description'] ) );
|
||||||
|
|
||||||
|
// Save FAQ Array
|
||||||
|
$questions = isset( $_POST['faq_question'] ) ? array_map( 'sanitize_text_field', $_POST['faq_question'] ) : array();
|
||||||
|
$answers = isset( $_POST['faq_answer'] ) ? array_map( 'wp_kses_post', $_POST['faq_answer'] ) : array(); // Allow basic HTML for answers
|
||||||
|
|
||||||
|
$faq_pairs = array();
|
||||||
|
for ( $i = 0; $i < count( $questions ); $i++ ) {
|
||||||
|
if ( ! empty( $questions[ $i ] ) && ! empty( $answers[ $i ] ) ) {
|
||||||
|
$faq_pairs[] = array(
|
||||||
|
'question' => $questions[ $i ],
|
||||||
|
'answer' => $answers[ $i ]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
update_post_meta( $post_id, '_seo_faq', $faq_pairs );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override the <title> tag.
|
||||||
|
*/
|
||||||
|
public function custom_title( $title ) {
|
||||||
|
if ( is_singular() ) {
|
||||||
|
$custom = get_post_meta( get_queried_object_id(), '_seo_title', true );
|
||||||
|
if ( ! empty( $custom ) ) return $custom;
|
||||||
|
}
|
||||||
|
return $title;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output Description & Keywords.
|
||||||
|
*/
|
||||||
|
public function output_meta_tags() {
|
||||||
|
if ( ! is_singular() ) return;
|
||||||
|
$id = get_queried_object_id();
|
||||||
|
if ( $desc = get_post_meta( $id, '_seo_description', true ) ) echo '<meta name="description" content="' . esc_attr( $desc ) . '" />' . "\n";
|
||||||
|
// (Optional: We leave keywords empty intentionally, but if you insist, uncomment below)
|
||||||
|
// if ( $kw = get_post_meta( $id, '_seo_keywords', true ) ) echo '<meta name="keywords" content="' . esc_attr( $kw ) . '" />' . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output Open Graph (Facebook/LinkedIn).
|
||||||
|
*/
|
||||||
|
public function output_og_tags() {
|
||||||
|
if ( ! is_singular() ) return;
|
||||||
|
$id = get_queried_object_id();
|
||||||
|
$post = get_post( $id );
|
||||||
|
$title = get_post_meta( $id, '_seo_title', true ) ?: $post->post_title;
|
||||||
|
$desc = get_post_meta( $id, '_seo_description', true ) ?: wp_trim_words( $post->post_content, 30 );
|
||||||
|
echo '<meta property="og:title" content="' . esc_attr( $title ) . '" />' . "\n";
|
||||||
|
echo '<meta property="og:description" content="' . esc_attr( $desc ) . '" />' . "\n";
|
||||||
|
echo '<meta property="og:url" content="' . esc_url( get_permalink() ) . '" />' . "\n";
|
||||||
|
echo '<meta property="og:type" content="article" />' . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output JSON-LD FAQ Schema (The secret weapon for AI search).
|
||||||
|
*/
|
||||||
|
public function output_faq_schema() {
|
||||||
|
if ( ! is_singular() ) return;
|
||||||
|
$id = get_queried_object_id();
|
||||||
|
$faq_data = get_post_meta( $id, '_seo_faq', true );
|
||||||
|
|
||||||
|
if ( empty( $faq_data ) || ! is_array( $faq_data ) ) return;
|
||||||
|
|
||||||
|
$schema = array(
|
||||||
|
'@context' => 'https://schema.org',
|
||||||
|
'@type' => 'FAQPage',
|
||||||
|
'mainEntity' => array()
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $faq_data as $faq ) {
|
||||||
|
$schema['mainEntity'][] = array(
|
||||||
|
'@type' => 'Question',
|
||||||
|
'name' => $faq['question'],
|
||||||
|
'acceptedAnswer' => array(
|
||||||
|
'@type' => 'Answer',
|
||||||
|
'text' => $faq['answer']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<script type="application/ld+json">' . json_encode( $schema, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) . '</script>' . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new Advanced_SEO_Meta();
|
||||||
Reference in New Issue
Block a user