PADI × Axelerant · PADI-403 · Canonical reference

Element Parity Matrix: blog · pros-blog · Drupal 11

Every editorial element, one row each: what it does on the Consumer Blog, how the Pros Blog differs, and the Drupal 11 implementation for the padi.com merger (/blog + /pros-blog sections, headless Next.js front end). Incorporates the most recently confirmed IA decisions β€” where this matrix and the IA document differ, this matrix is current.

βœ… built / decided / native parityπŸ”§ to build (scoped)⚠️ decision needed❌ lost / not migrated"Same" = identical to blog

Global settings

Elementblog.padi.compros-blog.padi.comDrupal 11
PlatformWP + PADI_Theme, 27 editorial pluginsWP + PADI_Pros (different theme), 26 pluginsOne Drupal 11 install at padi.com, two sections (/blog, /pros-blog) with separate bundles per section; headless Next.js front end (JSON:API)
Scale12,057 posts Β· 50 pages Β· 182 videos Β· 31.7k media files14,791 posts Β· 67 pages Β· 24 orphaned downloadsEverything editorially meaningful migrates (see per-type rows)
Languages14 active (WPML, directory URLs)19 active β€” adds el, fi, pl, tr, es-euβœ… All languages installed β€” union of both sites' sets incl. the custom es-eu locale; default content language read per item from WPML (not assumed English). Missing-translation URL handling β†’ Translations section
Front pageStatic page 37072; hero = latest storyStatic page 373803βœ… Section landings /blog + /pros-blog: home pages aliased to the section root, latest-articles listing embedded; hero via computed_hero over JSON:API
Archive page size6 posts/page9 posts/pageFE page[limit] per section β€” no admin knob (editors will ask)
CommentsFully disabled (plugin)Default-closed; 3 real legacy comments (+509 spam)βœ… Decided: comments are not migrated; comment module stays uninstalled
Display ads (Google Ad Manager)GPT slot /21864413585/blog.padi.com at the top of single posts (responsive size map, div-gpt-ad-*); skipped on front/archive/author-94/Japanese/promoted/amplifiedNoneFE logic; rules documented, no admin surface
Cache purgingCloudflare purge on save + Clear-Cache admin page + disappearing-block timed purgesNone visibleβœ… acquia_purge + purge (in project) β€” cache-tag invalidation on publish/unpublish; πŸ”§ FE revalidation webhook
Timezone / formatsAmerica/LA Β· "j F, Y"Sameβœ… Core regional settings
Audit trailActivity Log plugin β€” every admin actionSameπŸ”§ admin_audit_trail / event_log_track

Content types

Elementblog.padi.compros-blog.padi.comDrupal 11
ArticlesFull editorial set (Featured, Promoted/Amplified, Show-as-Updated, widgets injection)Plain posts β€” none of the blog's ACF side-fields; own Show-as-Updatedβœ… Separate bundles: blog_article + pros_blog_article β€” shared field storage, bundle-scoped permissions, unambiguous "which section am I publishing into"; side-fields as booleans/fields; presave parity built
Pages50; hierarchical + 6 templates; Landing template reveals ACF hero fields67; plain pagesβœ… Separate bundles: blog_page + pros_blog_page (field-for-field clone). Migrate publish + private (private β†’ archived); drafts skipped (8 blog + 1 pros). Triage: section landings β†’ Views routes; home pages β†’ aliased to /blog and /pros-blog with embedded latest View; obsolete duplicates dropped. ⚠️ WP template variants (FAQ/Landing/Full-width): need depends on design finalisation β€” designs not ready yet
VideosCPT: classic-editor-only, REST-invisible, no revisions; embed lives in bodyNoneβœ… Decided: all videos migrate as blog_article β€” served at /blog/<slug>; old /video/<slug> URLs 301 to the new alias
PADI WidgetsCPT injected into posts by category/post-ID/language rules; drag-orderedNoneβœ… Decided: migrated as Drupal blocks (block_content: padi_widget); πŸ”§ FE visibility evaluation; ordering β†’ weight field
Reusable blocks4 saved wp_block itemsβ€”βŒ Not migrated β€” PADI Widgets are the only blocks that migrate
Forms5 CF7 opt-in formsNone (22 legacy Jetpack feedback)βœ… Done: 5 webforms + prefapi handler + webform_rest; config translation pending per language set
Downloadsβ€”24 Download Monitor items; plugin uninstalled (orphaned)⚠️ Keep/drop decision (absent from the IA doc inventory)
Excluded CPTstribe_events/venue, calp_event (stale 2014), wpzoom, page-builder leftovers, nav/oembed/ACF-definition infrastructure❌ Intentionally not migrated

Content list (the editorial dashboard)

Elementblog.padi.compros-blog.padi.comDrupal 11
Ordering flow ⭐Scheduled on top β†’ published below β†’ unscheduled drafts float by last edit (verified; stated PADI requirement)Same (verified)βš οΈπŸ”§ Not default (sorts by last-updated only). Build: pinned "Scheduled" block above the list (config) or custom sort COALESCE(publish_on, changed) DESC (exact parity)
ColumnsTitle, languages, author, terms, date, Featured βœ“, Future Action, Date Updated, thumbnail, Unlisted + 7 Yoast columns; per-user togglesSimilar minus Premium Yoast columnsViews: any field as column/filter β€” site-wide config, ❌ no per-user Screen Options
Status tabsLive counts per status, per languageSameExposed filters without counts; one list across languages and both sections (filter by bundle); moderation_dashboard optional
Quick EditInline row editSame❌ None β€” full node form only
Bulk actionsFixed bulk-edit box + TrashSameβœ… Better: VBO β€” any field, any action
Trash30-day restoreSame❌ Delete is permanent β€” archived state covers soft-removal; training point
Editor choiceBlock or classic per postSame❌ One editor (CKEditor 5)
Cloneβ€”Duplicate Post (1-click clone)πŸ”§ entity_clone

Editor & per-post fields

Elementblog.padi.compros-blog.padi.comDrupal 11
CanvasGutenberg, 136 block types, or classic TinyMCEClassic-leaning + TinyMCE Advanced; MaxButtons shortcodesOne body field per article: CKEditor 5, single rich_blog text format with embed button; components inserted in-body via entity_embed + paragraphs_entity_embed; literal-text shortcodes dropped at migration
Component inventoryBootstrap grid, carousels (6 uses), overlay card, Yoast FAQ/How-to/TOC, Instagram feed, VR view, theme shortcodes, 33 embed providersOverlay (57 uses), latest-news, bootstrap setπŸ”§ padi:* component set built once, shared by both sections (button covers [button]/[maxbutton]/wp:button; gallery, embed, video, columns, media-text, FAQ, tabs, trending-posts, dd-top-posts, dd-categories, facebook-video, instagram, vrview, carousel, overlay, latest-news, bs-container/row/column)
Disappearing BlockTime-windowed content, 242 occurrences + scheduled cache purges at both boundariesβ€”πŸ”§ padi:disappearing-block component + scheduler-driven cache invalidation (scheduler replaces the WP purge cron)
Featured image~85% coverage; 550Γ—347 hard crop~36% coverageβœ… field_featured_image β†’ media:image; image styles + focal_point
Featured checkboxdd_is_featured β†’ front-page Featured cards (verified)❌ Noneβœ… Native promote flag; FE queries it
Show as UpdatedACF dd_updated_*, auto-set on first publish (verified)Plain metabox, padi_updated_* keysβœ… Fields + presave auto-init built; migration maps both key sets
Promoted / AmplifiedACF checkboxes β†’ ad suppression + body class (verified)❌ NoneBoolean fields + FE ad logic
Unlist post144 posts: hidden from listings/feeds/search, URL live, noindexSame plugin activeπŸ”§ Flag field; FE excludes from listings/queries (indexing + sitemap handling β†’ SEO section)
Scheduled actionsFuture Actions: unpublish/draft/trash/category-change at date (15 armed)6 saved workflows, all inactiveπŸ”§ scheduler (publish_on/unpublish_on); category-change needs small custom cron
SEO metaboxYoast Premium (keyphrase, Google preview, readability, AI, internal links, cornerstone)Yoast freefield_meta_tags (metatag) + field_yoast_seo (focus keyword migrates; snippet preview + keyword analysis). ❌ Readability scores, internal-link suggestions, cornerstone/orphaned analysis have no Drupal equivalent β€” socialize with SEO team
Social share togglesSassy per-post disables❌ NoneSharing plugins not migrated; FE-owned share buttons (build scope)
Draft sharingPublic Post Preview (expiring URL, noindex) + Share a DraftShare a Draft onlyπŸ”§ FE tokenized preview route with noindex; ⚠️ scope for day one

Statuses & editorial workflow

Elementblog.padi.compros-blog.padi.comDrupal 11
Workflow statesDraft / Pending (label only, no enforcement) / Scheduled / Published / Private / TrashSameβœ… Defined (PADI-49): Draft β†’ Initial Review β†’ Translate β†’ Final Review β†’ Published β†’ Archived β€” real gated transitions per role; πŸ”§ workflow config to land in repo
Status migration mappublish Β· draft Β· private Β· pending Β· future Β· auto-draftβœ… publishβ†’published Β· draftβ†’draft Β· privateβ†’archived Β· pendingβ†’initial_review Β· futureβ†’draft + scheduler date Β· auto-draft skipped
Scheduled publishingFuture-dated posts; wp-cron (+insurance plugin)Same, no insuranceπŸ”§ scheduler (full Future-Actions mapping β†’ Editor section)
Private / password325 private; password unused12 private pagesPrivate β†’ archived at migration; password-protection dropped
Sticky1 sticky postβ€”βœ… Native sticky flag (Featured checkbox β†’ Editor section)
Revert workflow impact ⭐Restore changes content only β€” never status/date/slug/terms/thumbnail/Yoast; ACF values do roll back; undoable (verified)Same core (verified); its Show-as-Updated does NOT roll back (plain metabox)Revert restores all fields incl. published flag β€” can unpublish a live node (opposite of WP) ⚠️; with the workflow, reverts can require re-approval (a gate WP can't express)

Revisions

Elementblog.padi.compros-blog.padi.comDrupal 11
Migration4,187 revisions52,246 revisionsβœ… Decided: all WordPress revisions migrate as node revisions (no date window)
StorageUnlimitedUnlimitedβœ… Unlimited default
CoverageTitle/body/excerpt + ACF values (editor saves)Title/body/excerptβœ… Better: every field revisioned (terms, media, SEO included)
Compare UISlider timeline, side-by-side, restoreSameDiff module (split/unified) β€” functional, less polished; no slider
VideosNo revisions at all (CPT lacks support)n/aβœ… Fixed by design β€” uniform revisions incl. migrated ex-videos
Autosave / locking60s autosave + browser recovery; core post lock w/ take-overSameπŸ”§ autosave_form + content_lock contrib
Per languageEach translation = own post = own revision streamSame⚠️ One shared revision graph across translations β€” editor training point

Translations (TMGMT + Crowdin)

Elementblog.padi.compros-blog.padi.comDrupal 11
ModelOne post per language, linked by trid; groups often incompleteSame modelOne node, per-language field values (content_translation, "translations independent" mode β€” per-translation status, author, dates); migration collapses trid groups per source language read from WPML
PipelineTranslation Dashboard batches β†’ ATE or in-admin translatorsSameβœ… Decided: TMGMT required; translations managed through Crowdin (tmgmt_crowdin connector β€” πŸ”§ to add to composer); jobs pushed from Drupal, synced back as translations; workflow's Translate state marks content out at Crowdin
Translator experienceATE: hosted side-by-side, MT prefill, TM, glossarySameCrowdin editor β€” comparable (TM, MT, glossary, QA checks, tasks/due dates)
Translator access10 per-language wp-admin roles (0 users hold them); 35 users carry the layered WPML translate capability46 users carry translate capability; no translator rolesβœ… Decided: no Drupal translator role β€” translators work only in Crowdin; review/publish of returned translations sits with content_manager / section editors
PrioritiesTranslation Priority taxonomy (Optional/Required/Not needed)Sameβœ… Not migrated β€” Crowdin tasks/due dates replace it
Duplicate modeSynced copies until "translate independently"Sameβœ… Core language fallback covers it
Missing translation URLWPML serves language fallback per configSameβœ… Decided: 301 to the source-language URL (keeps link equity)
UI strings / forms / menusWPML String Translation + Menus SyncSamelocale + config translation (webforms Γ— languages is the largest surface); menu link labels translate in place
Localized slugsEvery translation owns its slugSamepathauto per language from translated titles + preserve migrated aliases
Translation memoryYears of ATE TMSameπŸ”§ Seed Crowdin TM from migrated translation pairs β€” don't start empty

Taxonomy

Elementblog.padi.compros-blog.padi.comDrupal 11
VocabulariesCategories 368 (hierarchical, default "Diving" auto-assigned) + Tags 8,959 (flat)Categories 753 + Tags 7,088βœ… Decided: separate per-section vocabularies β€” blog categories/tags AND pros-blog categories/tags migrate independently. No cross-site merging or de-duplication. Hierarchy + term translations preserved; orphan terms dropped
Default category"Diving" auto-assigned when none chosenOwn defaultβœ… Presave default (same hook as primary-category default)
Primary categoryYoast picker (~9.6k posts)Yoast pickerβœ… field_primary_category + presave default (built)
Term archives/category/<slug>/, /tag/<slug>/ per siteSamePer-section Views routes: /blog/category|tag/<slug>/ and /pros-blog/category|tag/<slug>/ (per language with prefix)
Term slug changesNo auto-redirect (manual)Sameβœ… Better: pathauto + redirect auto-301
Translation Priority taxonomyWPML workflow metadataSameβœ… Not migrated (replaced by Crowdin tasks)
Category term 81 quirkHero fallback excludes "News & Events"Same code excludes "TecRec" (same ID, different term)βœ… Moot β€” the site-wide fallback hero is not carried over (empty authors get profile + message), so no exclusion rule is needed

Media

Elementblog.padi.compros-blog.padi.comDrupal 11
Entity model542k attachment rows (31.7k unique files) β€” WPML duplicates per language446k rows (~20k unique files)Adopted approach: per-language media entities retained where WP has translations (mirrors WPML); unique files transfer once (~52k files, WP resize derivatives skipped β€” Drupal regenerates). Alt/caption/description migrate from attachment fields
BundlesImages (~99%), PDFs/Office (13k PDFs on pros), local video files, ZIPsmedia: image Β· document Β· video_file Β· remote_video (future) Β· archive
Library UXGrid + list; year/month foldersSameβœ… One shared Media Library across sections (no section silo); files under public://blogs/Y/M + public://pros-blogs/Y/M; DAM integration = phase-2 re-homing of file URIs (no re-migration)
Image sizesthumbnail 200Γ—200 Β· archive 550Γ—347 Β· medium/large/core setthumbnail 150Γ—150 Β· archive 600Γ—400 Β· hero 2000Γ—720 (pros-only)~11 image styles: 8 shared + section-scoped variants for the two name collisions (blog_thumbnail/pros_thumbnail, blog_archive/pros_archive) + pros hero; focal_point replaces blind crops
Alt textOptional, inconsistentSameβœ… Can be made required (a11y win)
SVGAllowed (svg-support)SameπŸ”§ SVG media type + sanitization
Usage tracking"Uploaded to" column onlySameβœ… Better: entity_usage
Pixel editorBuilt-in crop/rotate/scaleSame❌ No core equivalent; focal point + styles cover real cases

Menus, widgets & chrome

Elementblog.padi.compros-blog.padi.comDrupal 11
Menus8 menus, 1 location; items picked from content lists; WPML Menus SyncOwn menu set, same mechanicsmenu_link_content (autocomplete links); labels translate in one menu; FE fetches by machine name (jsonapi_menu_items); nav_menu_item/wp_navigation rows not migrated as content
Widget areasSidebar, Footer 1–5, Mobile Language Chooser, Header Ad Block CodeSimilar classic areasFE components; reusable editorial content only via the padi_widget block bundle
Widget Logic2 live PHP rules (in_category(Americas), !language_is(en))Not activeRe-express in FE visibility logic
CustomizerHeader/Background modsSame + empty GA fieldGone β€” FE owns presentation

Users & roles

Elementblog.padi.compros-blog.padi.comDrupal 11 (target architecture)
Role set20 roles9 rolesβœ… Decided: administrator Β· blog_admin (above all blog/pros-blog roles) Β· content_editor (global: publish any section) Β· content_manager (publish/return/archive incl. translations) Β· blog_editor / blog_author Β· pros_blog_editor / pros_blog_author Β· authenticated. Bundle-scoped permissions come free from the split content types. πŸ”§ Role configs to land in repo (current content_editor config is author-grade and needs rework)
WP editor (69 / 91)Edit + publish everything on their site→ blog_editor / pros_blog_editor per source site (union if on both)
WP author + contributor (60 / 30)Own drafts; contributors can't publish→ blog_author / pros_blog_author — draft + submit to Initial Review; no publish
WP administrator (14 / 11)Full control→ administrator
Translatorstranslate capability layered on 35 users; per-language roles exist but hold 0 users46 layered; no translator rolesβœ… Decided: no Drupal translator role β€” translation happens in Crowdin; these users get Crowdin accounts (⚠️ any who also author content need an editorial role)
PADI Admin (Read Only) (8 / 7)Theme-enforced read-only (wp_die on POST)βœ… Decided: not migrated β€” Axelerant-internal accounts
SEO Manager/Editor, Revisor, Event ContributorRegistered; zero users hold them❌ Skipped
Subscribers + no-role users (1+40 / 5+25)Read-only or legacy authors→ authenticated; no-role users additionally blocked (preserves bylines on migrated content)
DeduplicationSame people hold accounts on both sitesMerge by email; blog username wins collisions; role set = union; authorship reassigned to the merged user; import users before content
End users / membersProfilePress stackβ€”βœ… Out of scope β€” Cognito on FE

Author landing pages

Elementblog.padi.compros-blog.padi.comDrupal 11
URL/author/<slug>/ (per language)Sameβœ… Decided: per-section routes β€” /blog/author/<slug> and /pros-blog/author/<slug> (+ language prefix); redirects map old author URLs per section
LayoutHero (author's latest) β†’ profile β†’ listing 6/page (newest skipped into hero)Hero only β€” archive.php lacks an is_author() branch (accident)βœ… Built: full layout on both sections (deliberate upgrade for pros) β€” design per padi.duckdiver.net/author/elliotpelling/; hero banner = the author's latest published post
Language behaviorPer-language hero+listing; author with posts only in another language β†’ fallback hero, profile hiddenHero per languagePer-language; no published post in the viewed language β†’ profile + graceful message (decided)
Author w/ no posts200 + site-latest fallback hero β€” never 404 for existing users (verified)Same (verified)βœ… Decided: show the author profile with a graceful message β€” no fallback hero, no 404 (improves on WP); unknown author β†’ 404 comes free
QuirksAuthor 43 avatar hidden; author 94 = no adsβ€”Not reproduced; FE decisions

URLs & redirects

Elementblog.padi.compros-blog.padi.comDrupal 11
URL patterns/<slug>/ at domain root; localized slugs per languageSamepadi.com/blog/<slug> Β· padi.com/pros-blog/<slug> Β· /<lang>/ prefix for non-English; pathauto per bundle+language; migrated localized aliases preserved
Domain moveblog.padi.compros-blog.padi.comOld domains 301 at DNS/CDN edge to padi.com sections; in-Drupal redirect module handles path-level mapping incl. _wp_old_slug history
Redirect storesRedirection: 6,137 (+regex +404 log) plus Yoast Premium auto-redirectsRedirection only⚠️ Decision needed: whether the WP redirect stores are migrated at all β€” not in scope by default. If yes: import + dedupe into redirect module, regex rules β†’ Cloudflare edge (redirect core has no regex). Structural redirects (old domains/paths β†’ new section URLs, slug history) are separate and remain in scope
Slug change behaviorYoast Premium auto-redirect offerManualβœ… Better: automatic 301 on alias change (nodes and terms)
404 monitoringRedirection 404 logSameβœ… redirect_404

SEO & indexability

Elementblog.padi.compros-blog.padi.comDrupal 11
Meta tagsYoast per-post + templatesSame (free tier)3 layers: per-node field_meta_tags (migrated from _yoast_wpseo_*) β†’ per-bundle token defaults (section branding: "| PADI Blog" vs "| PADI Pros") β†’ global defaults. Metatag-v2 computed field feeds Next.js generateMetadata over JSON:API
Indexable pagesPublished content, front, category/tag archives, author-with-posts (verified index,follow)SameFE emits robots from metatag data; parity per route type
NoindexedDate archives, search, empty-author, format archives (verified)SameFE route rules replicate
Unlisted postsnoindex on page but still in XML sitemap (144; verified leak)Same pluginβœ… Fix: FE noindex + sitemap exclusion together
Per-post noindex overrides46 published posts (editor-set)Same capabilityπŸ”§ Migrate into per-node metatag values or they go indexable
OG/Twitter imagesYoast social tab; few explicit overrides (~14 / ~38)SameOnly explicit overrides migrate; og:image falls back to featured image via tokens
Sitemapspost/page/video/category/tag/authorSame minus videoπŸ”§ simple_sitemap per language; excludes drafts/in-review/archived; hreflang alternates via metatag + content_translation
Structured dataYoast schema graphSameFE-composed JSON-LD (Article, WebPage/CollectionPage, BreadcrumbList, FAQPage from the FAQ component) from JSON:API fields; schema_metatag only if CMS-managed overrides are wanted
Yoast analysisPremium: readability, link suggestions, cornerstoneFree: keyword analysisfield_yoast_seo: focus keyword migrates; keyword/snippet analysis in-form. ❌ No readability score in the Drupal module (verified) β€” editorial-process substitute recommended
robots.txtAllow all + CCBot blockedAllow all, no CCBot block⚠️ FE robots.txt β€” one deliberate AI-crawler policy for both sections

Analytics & tracking

Elementblog.padi.compros-blog.padi.comDrupal 11
Google Tag ManagerGTM-MQW3Z7 β€” theme hardcodes on 404+search; plugin configured (prod)GTM-MQW3Z7 site-wide via plugin (verified)πŸ”§ GTM lives entirely in the Next.js front end (no google_tag module); per-section GA properties inside the container; exclude/separate preview routes
Direct GA (gtag)None β€” via GTMHardcoded gtag block with empty ID (no-op)n/a β€” FE/GTM only
Click eventsdata-event attributes (Hero/Featured/Read Article) β†’ gtag eventsSame markup patternFE analytics events on equivalent components
A/B testingVWO 262586 site-wide with anti-flickerNone⚠️ Decide VWO's future β€” nothing re-adds it by default
Facebook SDKSite-wideNone⚠️ FE decision
Popularity counterWP Popular Posts per-view logging (feeds trending)Same⚠️ Popularity source for trending components (GA-based or custom counter)
Untracked areaswp-admin; unpublished. Preview + unlisted URLs fire all trackersSameβœ… Opportunity: exclude preview from analytics cleanly