Jump to content

Recommended Posts

Posted

Hi all,

I've made the SeoNeo repo public as planned.

SEO Neo is a modern SEO module for ProcessWire that started as a practical fix for real-world canonical, pagination, and hreflang bugs on multilingual sites, and grew into a full replacement path for MarkupSEO and Seo Maestro.

GitHub: https://github.com/PeterKnightDigital/SeoNeo
Current version: 1.1.3 · Requires: ProcessWire 3.0.200+, PHP 8.1+

What makes it different
SeoNeo is a coordinator module, not a custom Fieldtype. It creates ordinary ProcessWire fields (Text, Textarea, URL, Checkbox, etc.), reads them via a configurable mapping, resolves fallbacks, and renders the <head> block.

That means:

  • Every SEO value is a real PW field — full multi-language support, selectors, import/export
  • No custom database schema or Fieldtype complexity
  • The SEO tab sits alongside your existing Content / Settings tabs

Add seoneo_tab to a template and save — the rest of the SEO fieldset (seoneo_preview, title, description, canonical, robots, etc.) is inserted automatically.

What it outputs
Full <head> SEO block in one call:

echo $page->seoneo;
// or
echo $page->seoneo->render();


Includes:

  • <title> with configurable format, separator, site name, pagination placeholders
  • Meta description, keywords, author
  • Canonical URL (with configurable URL-segment and pagination policy)
  • Robots meta (noindex/nofollow per page, auto-noindex for unpublished/hidden pages, site-wide defaults)
  • Granular Google robots directives (max-snippet, max-image-preview, etc.)
  • AI/LLM opt-out signals (noai, noimageai) — polite signals, not a substitute for blocking bots at HTTP/robots.txt level
  • Open Graph (title, description, url, type, site name, locale, image + dimensions/secure_url/type)
  • Twitter/X cards (auto summary vs summary_large_image)
  • Hreflang alternates with configurable BCP47 map (default=en-GB, de=de-AT, etc.)
  • Search-engine verification tags (Google, Bing, Yandex, Pinterest, Facebook, Baidu)
  • JSON-LD @graph emitter (Consider BETA IE works, but API/defaults may still change; hooks recommended for production-critical schema) 
  • Partial renders and resolved values are available too — flat API ($page->seoneo->renderOg()) or SeoMaestro-style namespaces ($page->seoneo->og->render()). Everything is hookable.

Editor / admin features
Bundled InputfieldSeoNeoPreview (installs with the module):

  • Live Google SERP preview that updates as you type
  • Desktop / mobile toggle — mobile truncates earlier (separate char budgets)
  • Multilingual language switcher on the preview card
  • Surface-aware character counters (green/amber/red zones, optional hard maxlength)
  • Per-page noindex/nofollow checkboxes
  • Optional NEO badge on the Wire tab — handy when running alongside MarkupSEO's also-named "SEO" tab during migration


Configuration highlights

  • Module config covers site name (per-language)
  • title format
  • smart field mapping with ancestor walk (*summary)
  • per-template defaults with placeholders
  • OG image field paths (including dotted paths like banner.image)
  • default OG image
  • locale map
  • Twitter handles
  • auto-inject position
  • canonical policy
    and more.
  • ProCache: documented and tested on cache-miss and cache-hit paths.

Migrating from MarkupSEO or Seo Maestro
You can run both modules during migration . You keep legacy fields on the template, copy values into seoneo_* fields at your own pace, then switch templates from $page->seo to $page->seoneo (shape is largely preserved). Migration is being worked on.

Watch for doubled <head> output if both modules auto-inject — disable auto-inject on whichever isn't authoritative yet.

Quick steps:

  1. Install SeoNeo (Modules → Refresh → Install)
  2. Add seoneo_tab to templates
  3. Copy field values (seo_description → seoneo_description, etc.)
  4. Rewrite template API calls
  5. Uninstall legacy module when ready

Full feature comparison and migration notes are in the README:
https://github.com/PeterKnightDigital/SeoNeo#migrating-from-markupseo-or-seo-maestro

Deliberately out of scope
SeoNeo focuses on <head> SEO coordination, not Swiss-Army-knife extras. For these, dedicated modules are a currently a better fit:

  • Sitemap → MarkupSitemap
  • Redirects → Jumplinks2 or ProcessRedirects
  • robots.txt editor → MarkupRobotsTxt or a template override
  • Analytics/GTM → MarkupGoogleTagManager or similar

(A PRO companion bundle with deeper editor tooling is planned separately)

Install
Copy SeoNeo to site/modules/
Modules → Refresh → Install SeoNeo
Add seoneo_tab to any template that needs SEO

Feedback very welcome. Especially from anyone migrating off MarkupSEO or Seo Maestro on multilingual or ProCache sites. Issues and PRs on GitHub are the best place for bugs and feature requests.

Cheers,
Peter

  • Like 2
  • Thanks 3
Posted

Here's a handy FAQ with the types of issues I needed to solve and are built into SEO Neo. 

FAQ (quick answers)

1. Can meta descriptions be filled automatically from existing page fields?
Yes. Smart field mapping and per-template defaults pull from fields like summary or body when the SEO description is left blank. Auto-resolved text is truncated to a configurable length at a word boundary.

2. Can page titles get a consistent suffix (e.g. “About us | My Company”)?
Yes. That is built into module config: site name, title format, and separator — no custom code needed for the usual pattern.

3. Can I override or extend resolution logic with hooks?
Yes. Individual resolvers (title, description, OG image, etc.) are hookable, so sites with non-standard rules can plug in custom logic without forking the module.

4. Does the admin SERP preview reflect auto-resolved values?
Mostly. On load it uses the same resolver chain as the frontend. If SEO fields are empty, the preview shows the computed title and description. If an editor types into the SEO fields, those values take precedence.

5. I am on MarkupSEO or Seo Maestro — can I migrate gradually?
Yes. Both legacy fieldsets can stay on the same template while you copy values across. The SeoNeo tab can show a small NEO badge so it stays distinct when both tabs are labelled “SEO”. Migration helper planned.

FAQ Longer versions

1. Automatic descriptions from content fields
A common requirement is that staff and blog pages should not need a separate meta description when summary or body already exists.

SeoNeo handles this in module config, not by asking editors to duplicate content:

Smart field mapping defines fallbacks when seoneo_description is empty — for example, try summary, then body.
Per-template defaults go further: a [blog-post] or [person] block can set description={summary|body} so only those templates use that chain.
Truncation applies to auto-resolved values only. Values typed directly into the SEO description field are left as-is. The max length is set once in module config.
There is also an ancestor walk prefix (*fieldname) if a section landing page should supply a default description for child pages.

For edge cases — inheriting a homepage description site-wide, template-specific truncation rules, or pulling from a custom settings page — hooks on the description resolver are the extension point.

2. Title suffixes and branded <title> patterns

Another frequent ask is a predictable title pattern: Page name | company.com

That is a first-class feature via site name, title format, and title separator in module config. Per-template defaults can influence the source part of the title (e.g. {long_title|title} on blog posts) while the suffix still comes from the global format.

Homepages that already store a fully branded title in the SEO field can use a hook on the title formatter to skip the automatic suffix on that one page.

3. Hooks for custom SEO logic
Sites with existing custom SEO logic often need to tweak titles, descriptions, or OG images based on template, page ID, or external settings.

SeoNeo splits resolution into hookable steps — reading and resolving individual values, formatting the final title, resolving OG image, hreflang, and so on — rather than one monolithic hook. Render methods are hookable too if you need to append tags rather than change resolved values.

4. What the SERP preview in the admin actually shows
The bundled SERP preview calls the same PHP resolvers the frontend uses on initial render. While editing, typed SEO values take precedence; empty SEO fields fall back to server-resolved values from page load.

One limitation: the live preview watches the SEO input fields, not every source field in the fallback chain. Editing summary will not update the preview until save/reload if the description comes from smart-map. A richer fallback-chain visualisation in the editor is on the PRO roadmap.

5. Multilingual sites
Multilingual support is a common question for any SEO module, especially when hreflang and locale tags need to stay in sync with ProcessWire’s language tabs.

SeoNeo is built around native PW language-aware fields, not a separate storage layer. Each seoneo_* field behaves like any other translatable field — editors fill in SEO values per language tab, and the resolver chain returns the value for the currently active language on the frontend (or whichever language you switch $user->language to in PHP).

Configuration for multilingual output:

  • Per-language site name — override the global site name per language (e.g. de=Mein Beispiel). Used in title formatting, template defaults, and og:site_name.
  • Locale map — map PW language names to BCP47 codes (default=en-GB, de=de-AT, etc.). Powers og:locale, og:locale:alternate, and hreflang codes.
  • Hreflang alternates — emitted per language with correct URLs, including x-default pointing at the default-language URL. Segment and pagination handling matches the canonical URL policies.


In the page editor:

  • The SERP preview includes a language switcher (on multilingual sites) so editors can preview each language’s title and description without leaving the current PW language tab.
  • Resolved values in the preview use the same per-language fallback chain as the live site, including the localised URL in the breadcrumb.
  • Desktop/mobile toggle and character counters apply per surface regardless of language.
  • Smart-map and template defaults respect language context too — a German summary field resolves when the German language is active, not a mixed default.

For sites with unusual language setups (custom domain-per-language, non-standard hreflang codes, or locale rules that differ from PW’s language names), the hreflang and locale resolvers are hookable.

6. Migrating from MarkupSEO or Seo Maestro
Both legacy modules can stay installed while you move page by page:

  1. Install SeoNeo (InputfieldSeoNeoPreview installs with it).
  2. Add seoneo_tab to templates — remaining SEO fields insert automatically on save.
  3. Copy legacy values at your own pace.
  4. Switch templates from $page->seo to $page->seoneo when ready.
  5. Uninstall the legacy module when frontend output is fully on SeoNeo.


Watch for doubled <head> output if both modules auto-inject meta tags.

  • Thanks 2
Posted
8 minutes ago, Peter Knight said:

Thanks 🙏 Plenty of choice these days so feel free to find something that suits you. Also @maximus has a very comprehensive SEO module too. 

Yeah I just installed this module too and when comparing your module with that from maximus (and the old trusty Seo Maestro) there is no perfect "all in one" solution. Each module does something better than the other and vice versa! I think it is still mandatory to try out what module fits for your projects and fills out the gaps in your personal workflow.

For example:

I like that @maximus module includes sitemap generation and redirect handling

I like that your module offers per-template defaults 

and so on...

  • Like 3
Posted

Yes true. I purposely omitted sitemap generators in this phase. A significant volume of issues on the SEO threads are actually site map related so if I’m going to support it, I want it to work properly. And also I’m a big fan of Ryan’s pro sitemap module and simply prefer not to overlap existing modules that are actively developed. But it I’m not saying “never” 😉

  • Like 3
Posted

I.NEED.TO.TRY.THIS!

Congrats on this module. It looks/reads awesome. 
Read through more details on Github and I'm in love.

Need to try out how this plays out in a real project and how steep the learning curve is but... [chefskiss-emoji.gif]

  • Haha 1
Posted
6 hours ago, wbmnfktr said:

I.NEED.TO.TRY.THIS!

Congrats on this module. It looks/reads awesome. 
Read through more details on Github and I'm in love.

Need to try out how this plays out in a real project and how steep the learning curve is but... [chefskiss-emoji.gif]

Please do 🙂. I can’t guarantee everything is flawless at this early stage but I can guarantee speedy fixes. LMK where you can see areas for improvement.

P

  • Like 1
Posted

Took the time and installed SEO Neo to one my larger and most complete side-projects, and so far I am really impressed what can be done with this out of the box. The depth and customisation feels great on the first look. Might need to dig deeper into all the settings and options, but WOW!

BUT...

I noticed that auto-inject didn't work. Tried that in another instance that's almost clean without any other modules or whatever.
I probably missed something at some point. Got it working in both instances nonetheless. I might try it in a clean environment again, but as it is not a show-stopper for me I won't lose another thought about it.

Using the $page->seoneo->render()  worked everywhere, even in Twig (TemplateEngineFactory) with {{ page.seoneo.render() }}.
NICE!

 

One other thing I'm not sure about is adding SEO Neo fields to templates.
The module creates quite a few fields, but there is no option to single-click/single-action add them to a template of choice. Sure, in total only a minute or two to add them manually to ONE template but on larger sites with a lot of templates... well. I helped myself, added all fields to a new template and imported that new template into existing templates. More a hack, than a workflow, but at the end fields were in their right spot.

 

In terms of Schema/JSON+LD: what other schemas are planned or how would I add custom ones?
Looked into the docs but couldn't spot a reference to custom types like recipe, book, event, real estate / or related schemas.

 

Overall... migrating from a custom solution to SEO Neo is probably doable in a few hours with this very special project.
Luckily we have way better AI support now so it might be that all the Claudias out there can assist.

 

First impression was great. 
Will probably move that project over to SEO Neo this weekend.

Posted

Hi @wbmnfktr

Thanks so much for the feedback, and I'm glad you like it 🙂
I can only take credit for the planning and direction, but I put a lot of thought into it, and I'm glad to hear it's working (kinda') ok.

On the specifics...

Quote

I noticed that auto-inject didn't work.

That's interesting because I had a lot of issues getting it to work on the first pass. It turned out to be a non-related PHP error in my <HEAD> include was blocking the auto-inject. Once I cleared that, it worked. But I will now triple-check. Maybe hold off until I can come back to you tomorrow?
I might have mistakenly assumed the issue was closed by "fixing" an unrelated event.

Quote

One other thing I'm not sure about is adding SEO Neo fields to templates.

I'm not sure I'm understanding correctly here, so apologies for starting from scratch.

Yes, the module adds several fields between the SEO open and close tabs.

I was thinking that manually adding these to every template would be a chore for people; the fields should be auto-added once you manually add the SEO open and close tab, and then save. LMK if I misunderstood there.

Would you find it useful to pick the templates from the SeoNeo module config page (bulk add in one save), or do you prefer the standard PW workflow of opening each template individually?

Quote

In terms of Schema/JSON+LD: what other schemas are planned or how would I add custom ones?
Looked into the docs but couldn't spot a reference to custom types like recipe, book, event, real estate / or related schemas.

I'm quoting the following from the docs and AI (spot the em-dashes) 🙂
What ships today (built-in):

Organization, WebSite, WebPage, Article, Person, BreadcrumbList — auto-generated as a linked @graph
How to add custom types (Recipe, Event, Product, RealEstate, etc.) right now:

Hook ___getJsonLd() in your site's ready.php or a custom module:

$wire->addHookAfter('SeoNeo::getJsonLd', function(HookEvent $e) {
    $page = $e->arguments(0);
    if($page->template->name !== 'recipe') return;
    
    $data = $e->return;
    $data['@graph'][] = [
        '@type' => 'Recipe',
        'name' => $page->title,
        'description' => $page->summary,
        'prepTime' => 'PT' . $page->prep_time . 'M',
        'cookTime' => 'PT' . $page->cook_time . 'M',
        'image' => $page->image ? $page->image->httpUrl : '',
    ];
    $e->return = $data;
});

What's planned (future, not committed to a version):

A higher-level API — $page->seoneo->schema('Recipe', [...]) — with per-type hookable helpers. It's in the roadmap (section J in the backlog) but deferred because the API shape isn't stable enough to ship without risking breaking changes. The hook approach above is the recommended production path for now.

Also I believe my own docs are fuller in detail:
https://www.peterknight.digital/docs/seoneo/1.x/structured-data-json-ld/

Anywho LMK if any of those are useful answers and feel free to DM me too.
Cheers
P
 

  • Thanks 1
Posted
5 minutes ago, Peter Knight said:

Maybe hold off until I can come back to you tomorrow?

No hurries... it works and as I use Twig, I need to manually add it anyway.

6 minutes ago, Peter Knight said:

the fields should be auto-added once you manually add the SEO open and close tab, and then save.

Gave it a try... and it actually does work. Either this is a new behaviour or I never tried to do it this way.
That's working fine for me. Even easier than importing from another template as I did.

8 minutes ago, Peter Knight said:

Would you find it useful to pick the templates from the SeoNeo module config page (bulk add in one save), or do you prefer the standard PW workflow of opening each template individually?

As it turned out to be really easy I wouldn't really need the option to check a template and import the fields - I guess.
The PW workflow fits me perfectly fine as I need to migrate each template after the other, in my existing project.
In new projects this might be a bit different, but I can't really tell for now.
Let's keep it as it is, as it's working and proven PW way of doing things.

14 minutes ago, Peter Knight said:

What ships today (built-in):

Organization, WebSite, WebPage, Article, Person, BreadcrumbList — auto-generated as a linked @graph

I saw that!

14 minutes ago, Peter Knight said:

How to add custom types (Recipe, Event, Product, RealEstate, etc.) right now:
Hook ___getJsonLd() in your site's ready.php or a custom module

image.png.73bfb503f61f75f3cdcb007ff7d3db9e.png

But your provided example $wire->addHookAfter('SeoNeo::getJsonLd', function(Hoo[...] would totally fit my needs!

 

Your docs seem to be more complete or detailed. For now everything should be doable in my project. Some hooks, some manual editing on my side and my site... could be perfect. I hope I get this done this weekend to report back.

 

20 minutes ago, Peter Knight said:

Anywho LMK if any of those are useful answers and feel free to DM me too.

This helped a lot! I will let you know when there a road-blockers or show-stoppers. 

 

  • Like 1
Posted

SeoNeo 1.1.4 is live.

Pushed to: PeterKnightDigital/SeoNeo (GitHub)
Release: https://github.com/PeterKnightDigital/SeoNeo

SeoNeo 1.1.4 — what's new

Resilience.

If anything goes wrong while building the SEO meta block, the page now still renders cleanly — visitors see normal content and styles, only the SEO tags are absent on that one page. Previously, certain failures could blank out the entire <head> and break the page layout.

New seoneo log.

When something does fail, SeoNeo writes a one-liner to site/assets/logs/seoneo.txt (also visible at Setup → Logs → seoneo in the admin) telling you which page tripped and what the error was. The log file is created on demand, so clean installs that never hit a problem won't see it appear.

Better setup feedback.

The auto-inject checkbox in module config now warns you if no template has seoneo_tab yet — the most common reason new users see no SEO output on the front-end.

Docs.

The README's Installation section now spells out that you only need to add seoneo_tab to a template (the rest of the SEO fields are auto-inserted on save — a 1.1.0 feature that was easy to miss), and the JSON-LD section has a worked example for adding custom Schema.org types like Recipe or Event via a hook.

Backwards-compatible — drop in over 1.1.3, hit Modules → Refresh, you're done.

  • Thanks 1
Posted
3 minutes ago, Peter Knight said:

The auto-inject checkbox in module config now warns you if no template has seoneo_tab yet — the most common reason new users see no SEO output on the front-end.

That's the reason why it didn't work on first try on my instances. 🙈

  • Like 1

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...