@@ -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
|
||||
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.
|
||||
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.
|
||||
|
||||
## What it does
|
||||
## Features
|
||||
|
||||
On every push to `main`, the workflow at
|
||||
[`.gitea/workflows/wp-plugin-release.yml`](.gitea/workflows/wp-plugin-release.yml) runs:
|
||||
- Custom SEO title field for posts and pages
|
||||
- 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
|
||||
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.
|
||||
## What the plugin adds
|
||||
|
||||
## Requirements
|
||||
When editing a post or page, you can fill in:
|
||||
|
||||
- A **Gitea Actions runner** (`act_runner`) registered on your instance.
|
||||
- The runner image needs `rsync`, `zip`, and `git` available (standard on
|
||||
`ubuntu-latest`-style runner images).
|
||||
- A **repo secret** named `RELEASE_TOKEN` — a Gitea access token with
|
||||
repo write / release permissions. Add it under
|
||||
**Repo → Settings → Secrets**.
|
||||
- SEO Title
|
||||
- Meta Description
|
||||
- FAQ questions and answers
|
||||
|
||||
The plugin then outputs:
|
||||
|
||||
- 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
|
||||
|
||||
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
|
||||
/**
|
||||
* Plugin Name: My Cool Plugin
|
||||
* Version: 1.3.0
|
||||
*/
|
||||
```
|
||||
## Notes
|
||||
|
||||
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