PHPackages                             marshallu/mu-seo - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. marshallu/mu-seo

ActiveWordpress-plugin[Utility &amp; Helpers](/categories/utility)

marshallu/mu-seo
================

A lean SEO plugin for Marshall University WordPress sites.

v1.3.3(1mo ago)051MITPHPPHP &gt;=8.3

Since Feb 25Pushed 1mo ago1 watchersCompare

[ Source](https://github.com/marshallu/mu-seo)[ Packagist](https://packagist.org/packages/marshallu/mu-seo)[ Docs](https://github.com/marshallu/mu-seo)[ RSS](/packages/marshallu-mu-seo/feed)WikiDiscussions main Synced 1mo ago

READMEChangelogDependencies (16)Versions (10)Used By (0)

MU SEO
======

[](#mu-seo)

A lean SEO plugin for Marshall University's WordPress sites.

- **Package:** `marshallu/mu-seo`
- **Type:** WordPress plugin
- **Requires:** ACF Pro

---

Features
--------

[](#features)

- Custom SEO title and meta description per post/page
- Canonical URL override
- Robots meta tag control (noindex / nofollow) per post/page
- Open Graph and Twitter Card meta tags with a multi-step image fallback chain
- Site-wide options page for Twitter handle and default social image
- JSON-LD schema markup for posts (Article) and pages (WebPage)
- `mu_seo_schema` filter for adding or modifying schema on custom post types
- `mu_seo_og_type` filter for overriding the OG type on custom post types
- `mu_seo_og_image_id` filter for providing a social image on custom post types
- Yoast SEO migration tool (WP-CLI command + admin UI)

---

Installation
------------

[](#installation)

### Manual Installation

[](#manual-installation)

Upload the plugin directory to `wp-content/plugins/` and activate it from the WordPress admin.

```
wp-content/
└── plugins/
    └── mu-seo/
        ├── mu-seo.php
        └── includes/
            └── ...

```

Then go to **Plugins** in the WordPress admin and activate **MU SEO**.

### Composer Installation

[](#composer-installation)

```
composer require marshallu/mu-seo
```

Composer will install the plugin to `wp-content/plugins/mu-seo/`. Activate it from the WordPress admin or via WP-CLI:

```
wp plugin activate mu-seo
```

Composer dependencies (PHPCS, WPCS, ACF stubs) are dev-only and not required in production.

---

ACF Field Reference
-------------------

[](#acf-field-reference)

All per-post fields appear in the **SEO** meta box on every public post type edit screen. The box has two tabs.

### SEO Tab

[](#seo-tab)

FieldACF NameTypeNotesSEO Title`mu_seo_title`TextOverrides the `` tag and og:title. Falls back to the post title.Meta Description`mu_seo_description`TextareaOverrides the meta description and og:description. Falls back to the post excerpt.Canonical URL`mu_seo_canonical`URLOverrides the canonical link and og:url. Falls back to the permalink.Robots`mu_seo_robots`CheckboxCheck `noindex`, `nofollow`, or both to output a robots meta tag. Leave blank for default crawl behavior.### Social / Open Graph Tab

[](#social--open-graph-tab)

FieldACF NameTypeNotesSocial Image`mu_seo_og_image`Image (returns ID)Overrides the image used in og:image and Twitter card. See fallback chain below.OG Type`mu_seo_og_type`Select`article` or `website`. When left blank, defaults to `article` for posts and `website` for all other post types. Can be overridden per post type via the `mu_seo_og_type` filter.Twitter Card Style`mu_seo_twitter_card`Select`summary_large_image` (default) or `summary`.### Options Page

[](#options-page)

Located at **Settings &gt; SEO Settings**.

FieldACF NameTypeNotesTwitter / X Handle`mu_seo_twitter_handle`TextInclude the `@` symbol, e.g. `@MarshallU`. Populates `twitter:site`.Default Social Image`mu_seo_default_og_image`Image (returns ID)Fallback image when a post has no featured image or hero image.---

Social Image Fallback Chain
---------------------------

[](#social-image-fallback-chain)

When resolving the image for `og:image`, `twitter:image`, and JSON-LD schema, the plugin walks this chain and uses the first match:

1. **Post-level ACF override** — `mu_seo_og_image` field on the post
2. **Featured image** — `get_post_thumbnail_id()`
3. **Hero block image** — parsed from the first `acf/hero` block in post content (see below)
4. **`mu_seo_og_image_id` filter** — lets themes/plugins provide an image for custom post types
5. **Site default** — `mu_seo_default_og_image` from the options page

If no image is found, the image tags are omitted entirely.

### Hero Block Image Extraction

[](#hero-block-image-extraction)

The plugin parses the `acf/hero` block's saved `attrs.data` to find the image ID. The `hero_type` field determines which key is read:

`hero_type`Image source key`static``hero_image_image``random``hero_images_0_image` (first row)`video` / `videourl``video_video_thumbnail``none` / `color`No image---

Head Output
-----------

[](#head-output)

The following tags are output in `wp_head` on singular pages only. Nothing is output on archives, the home page, or 404s.

WordPress core's `rel_canonical` hook is removed — the canonical link is managed entirely by MU SEO.

### Meta Tags (`MU_SEO_Head`, priority 2)

[](#meta-tags-mu_seo_head-priority-2)

```

```

### Open Graph and Twitter Card (`MU_SEO_Social`, priority 1)

[](#open-graph-and-twitter-card-mu_seo_social-priority-1)

```

```

### JSON-LD Schema (`MU_SEO_Schema`, priority 2)

[](#json-ld-schema-mu_seo_schema-priority-2)

**Posts** receive `Article` schema:

```
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "...",
  "description": "...",
  "url": "...",
  "datePublished": "2024-01-01T00:00:00+00:00",
  "dateModified": "2024-01-01T00:00:00+00:00",
  "author": { "@type": "Person", "name": "..." },
  "publisher": { "@type": "Organization", "name": "..." },
  "image": { "@type": "ImageObject", "url": "...", "width": 1200, "height": 630 }
}
```

**Pages** receive `WebPage` schema:

```
{
  "@context": "https://schema.org",
  "@type": "WebPage",
  "name": "...",
  "description": "...",
  "url": "...",
  "datePublished": "2024-01-01T00:00:00+00:00",
  "dateModified": "2024-01-01T00:00:00+00:00",
  "publisher": { "@type": "Organization", "name": "..." },
  "primaryImageOfPage": { "@type": "ImageObject", "url": "...", "width": 1200, "height": 630 }
}
```

---

Developer Hooks
---------------

[](#developer-hooks)

### `mu_seo_post_types`

[](#mu_seo_post_types)

Filters the list of post types that receive the SEO and Social field group. The default is all post types registered with `public => true`. Use this to add post types with a UI but no public archive, or to remove post types that should not have SEO fields.

**Parameters:**

ParameterTypeDescription`$post_types``string[]`Array of post type slugs.**Examples:**

Add a non-public CPT:

```
add_filter( 'mu_seo_post_types', function( $post_types ) {
    $post_types[] = 'faculty';
    $post_types[] = 'program';
    return $post_types;
} );
```

Remove a post type:

```
add_filter( 'mu_seo_post_types', function( $post_types ) {
    return array_diff( $post_types, array( 'attachment' ) );
} );
```

---

### `mu_seo_og_image_id`

[](#mu_seo_og_image_id)

Filters the resolved social image attachment ID after the built-in fallback chain (ACF override → featured image → hero block) and before the site-wide default. Use this to provide a post-type-specific image source for CPTs that don't use featured images or the hero block.

The filter receives the ID resolved so far — return it unchanged to pass through, or return a different attachment ID to override.

**Parameters:**

ParameterTypeDescription`$image_id``int`Attachment ID resolved so far, or `0` if nothing found yet.`$post_id``int`The current post ID.**Example** — use a custom ACF field as the social image for a profiles CPT:

```
add_filter( 'mu_seo_og_image_id', function( $image_id, $post_id ) {
    if ( $image_id || ! is_singular( 'mu_profile' ) ) {
        return $image_id;
    }
    $headshot = get_field( 'profile_headshot', $post_id );
    return $headshot ? absint( $headshot ) : $image_id;
}, 10, 2 );
```

---

### `mu_seo_og_type`

[](#mu_seo_og_type)

Filters the default `og:type` for the current post. Runs only when the per-post ACF field is blank. Use this to assign the correct OG type to custom post types without editing MU SEO directly.

Valid OG types include `article`, `website`, and `profile`. See [ogp.me](https://ogp.me/#types) for the full list.

**Parameters:**

ParameterTypeDescription`$type``string`The default type. `article` for posts, `website` for everything else.`$post_id``int`The current post ID.**Example** — set `profile` for a people/profiles CPT:

```
add_filter( 'mu_seo_og_type', function( $type, $post_id ) {
    if ( is_singular( 'mu_profile' ) ) {
        return 'profile';
    }
    return $type;
}, 10, 2 );
```

---

### `mu_seo_schema`

[](#mu_seo_schema)

Filters the JSON-LD schema array before it is encoded and output. Runs on every singular page. For unhandled post types (not `post` or `page`) the initial `$schema` value is an empty array, giving you a clean slate to build from.

**Parameters:**

ParameterTypeDescription`$schema``array`The schema array. Empty for unhandled post types.`$post_id``int`The current post ID.`$post_type``string`The current post type slug.**Return `array`** — return an empty array to suppress output entirely.

**Examples:**

Add schema for a custom post type:

```
add_filter( 'mu_seo_schema', function( $schema, $post_id, $post_type ) {
    if ( 'event' !== $post_type ) {
        return $schema;
    }

    return array(
        '@context'  => 'https://schema.org',
        '@type'     => 'Event',
        'name'      => get_the_title( $post_id ),
        'startDate' => get_field( 'event_start_date', $post_id ),
        'location'  => array(
            '@type' => 'Place',
            'name'  => get_field( 'event_location', $post_id ),
        ),
    );
}, 10, 3 );
```

Append a property to the default schema:

```
add_filter( 'mu_seo_schema', function( $schema, $post_id, $post_type ) {
    if ( ! empty( $schema ) && 'post' === $post_type ) {
        $schema['articleSection'] = get_field( 'category_label', $post_id );
    }
    return $schema;
}, 10, 3 );
```

Suppress schema on a specific page:

```
add_filter( 'mu_seo_schema', function( $schema, $post_id, $post_type ) {
    return 42 === $post_id ? array() : $schema;
}, 10, 3 );
```

---

Yoast SEO Migration
-------------------

[](#yoast-seo-migration)

MU SEO includes a migration tool for moving Yoast SEO post meta and global options into MU SEO's ACF fields. Existing MU SEO values are never overwritten.

### What gets migrated

[](#what-gets-migrated)

**Per-post meta:**

Yoast meta keyMU SEO field`_yoast_wpseo_title``mu_seo_title``_yoast_wpseo_metadesc``mu_seo_description``_yoast_wpseo_canonical``mu_seo_canonical``_yoast_wpseo_meta-robots-noindex` / `nofollow``mu_seo_robots``_yoast_wpseo_opengraph-image-id``mu_seo_og_image`Values containing Yoast template variables (`%%title%%`, etc.) are skipped. If a URL-only OG image is stored, the tool attempts to resolve it to a WordPress attachment ID via `attachment_url_to_postid()`.

**Global options** (from `wpseo_social`):

Yoast optionMU SEO field`twitter_site``mu_seo_twitter_handle` (options page)`og_default_image_id``mu_seo_default_og_image` (options page)### WP-CLI

[](#wp-cli)

The migration command requires a site ID, making it safe for multisite use.

```
wp mu-seo migrate-yoast  [--dry-run] [--post-type=] [--per-page=] [--verbose]
```

**Arguments:**

ArgumentDescription``**Required.** Numeric ID of the site to migrate.**Options:**

OptionDescription`--dry-run`Preview changes without writing anything.`--post-type=`Comma-separated list of post types to migrate. Defaults to all public post types.`--per-page=`Batch size for post queries. Default: `100`.`--verbose`Print a line for every field action (migrated, conflict, skipped).**Examples:**

```
# Dry run on site 2
wp mu-seo migrate-yoast 2 --dry-run

# Migrate only posts and pages on site 5
wp mu-seo migrate-yoast 5 --post-type=post,page

# Full migration with per-field output
wp mu-seo migrate-yoast 3 --verbose
```

**Verbose output example:**

```
Post 42:
  mu_seo_title:                  migrated → My Page Title
  mu_seo_description:            skipped (conflict)
  mu_seo_canonical:              skipped (empty or variable)
  mu_seo_robots:                 migrated → noindex
  mu_seo_og_image:               migrated → attachment 187
Options:
  mu_seo_twitter_handle:         migrated → @MarshallU
  mu_seo_default_og_image:       skipped (empty)
Success: Done. Posts: 3 migrated, 1 skipped conflicts, ...

```

### Admin UI

[](#admin-ui)

The migration tool is also available at **Tools &gt; MU SEO Migration**. It runs the same migration logic without any options — all public post types, no dry run. Results are shown on the same page after completion.

---

Development
-----------

[](#development)

```
# Install dev dependencies
composer install

# Check coding standards
./vendor/bin/phpcs --standard=WordPress .

# Auto-fix coding standards violations
./vendor/bin/phpcbf --standard=WordPress .
```

All code follows [WordPress Coding Standards](https://github.com/WordPress/WordPress-Coding-Standards). Functions, hooks, and globals are prefixed `mu_seo_`.

---

File Structure
--------------

[](#file-structure)

```
mu-seo/
├── mu-seo.php                      # Plugin entry point
├── includes/
│   ├── class-mu-seo.php            # Core singleton, bootstraps all classes
│   ├── class-mu-seo-fields.php     # ACF field group (SEO + Social tabs)
│   ├── class-mu-seo-head.php       # Outputs title, description, robots, canonical
│   ├── class-mu-seo-options.php    # ACF options page (Settings > SEO Settings)
│   ├── class-mu-seo-social.php     # Outputs OG and Twitter Card tags
│   ├── class-mu-seo-schema.php     # Outputs JSON-LD schema
│   └── class-mu-seo-migrate.php    # Yoast SEO migration (WP-CLI + admin UI)
└── composer.json

```

###  Health Score

44

—

FairBetter than 92% of packages

Maintenance92

Actively maintained with recent releases

Popularity12

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity55

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~4 days

Total

9

Last Release

38d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/34515c639712e2e66d9e7e60f2d3eb4cebc1a048b645b82458d553964f125c26?d=identicon)[mccomaschris](/maintainers/mccomaschris)

---

Top Contributors

[![mccomaschris](https://avatars.githubusercontent.com/u/26128?v=4)](https://github.com/mccomaschris "mccomaschris (20 commits)")

---

Tags

pluginwordpressseo

###  Code Quality

Static AnalysisPHPStan

Code StylePHP\_CodeSniffer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/marshallu-mu-seo/health.svg)

```
[![Health](https://phpackages.com/badges/marshallu-mu-seo/health.svg)](https://phpackages.com/packages/marshallu-mu-seo)
```

###  Alternatives

[sybrew/the-seo-framework-extension-manager

A WordPress plugin that allows you to manage extensions for The SEO Framework.

8490.3k](/packages/sybrew-the-seo-framework-extension-manager)[afragen/git-updater

A plugin to automatically update GitHub, Bitbucket, GitLab, or Gitea hosted plugins, themes, and language packs.

3.3k1.6k](/packages/afragen-git-updater)[webdevstudios/cmb2-attached-posts

Custom field for CMB2 for creating post relationships.

13565.5k](/packages/webdevstudios-cmb2-attached-posts)[iceicetimmy/acf-post-type-selector

Post type selector for Advanced Custom Fields.

559.0k](/packages/iceicetimmy-acf-post-type-selector)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
