| title | Extending NeoWiki |
|---|---|
| order | 7 |
NeoWiki exposes extension points so other MediaWiki extensions can add custom Property Types, contribute page metadata to the graph, and reuse NeoWiki's UI. This page is the reference for those extension points and the APIs extensions build on.
NeoWiki concepts referenced here — Subject, Schema, Property Type, Page Property — are defined in the Glossary.
RedHerb is a minimal, test-backed example extension shipped in the NeoWiki repository. NeoWiki's own tests exercise it, so its examples stay working. Each extension point below links to the RedHerb file that demonstrates it, so the fastest start is to copy the relevant RedHerb file and adapt it.
NeoWiki is pre-1.0. Every extension point on this page is alpha and may change without notice until 1.0.
An extension that builds on NeoWiki declares the dependency in its extension.json:
"requires": {
"extensions": {
"NeoWiki": "*"
}
}Most backend extension points are registered through the NeoWikiRegistration hook, which hands you a
NeoWikiRegistrar:
"Hooks": {
"NeoWikiRegistration": "ProfessionalWiki\\MyExt\\MyExtHooks::onNeoWikiRegistration"
}public static function onNeoWikiRegistration( NeoWikiRegistrar $registrar ): void {
$registrar->addPropertyType( new ColorType() );
$registrar->addPagePropertyProvider( new StaticPagePropertyProvider() );
}Full example: src/RedHerbHooks.php.
A Property Type defines a kind of structured value — its Value Type, validation, and Display Attributes. Implement
the PropertyType interface, paired with a class extending PropertyDefinition that holds the type-specific
definition fields, and register it with NeoWikiRegistrar::addPropertyType() (see "Getting started" above). The
linked example shows the methods to implement.
Example: src/ColorType.php
(implements PropertyType) and
src/ColorProperty.php
(extends PropertyDefinition).
If your Property Type stores a value that isn't already a Neo4j scalar, also register a builder that converts it, keyed by the Property Type name:
$registrar->addNeo4jValueBuilder( ColorType::NAME, static fn ( $value ) => $value->toScalars() );Page Property Providers contribute key/value metadata to the Page node in the graph (queryable via Cypher;
Neo4j is currently the only graph backend). They run when a revision is stored for a page carrying Subject
data (including undeletions), and again for such pages when the graph is rebuilt with the
RebuildGraphDatabases maintenance script. Pages without Subject data are not stored in the graph, so
providers are never invoked for them. Implement PagePropertyProvider:
class StaticPagePropertyProvider implements PagePropertyProvider {
public function getProperties( PagePropertyProviderContext $context ): array {
return [ 'myext_reviewState' => 'approved' ];
}
}Register with NeoWikiRegistrar::addPagePropertyProvider(). The context exposes the page id, title,
creation and modification times, categories, and last editor, as well as the page content and its parse
products, so providers can derive Page Properties from the page content without re-fetching or re-parsing it.
To derive Page Properties from the content, prefer the parse products: categories and
parserProperties — the MediaWiki page properties recorded during parsing (e.g. those a parser hook
sets via ParserOutput::setPageProperty). These are template-expansion-safe and robust. (Note that
parserProperties are an input from MediaWiki's parse; they are not the NeoWiki Page Properties this
provider returns.) The raw main slot content and its contentModel are the fallback: scraping
raw wikitext is fragile — reach for them mainly when handling a custom, non-wikitext content model that
the parse products do not cover. Example:
src/StaticPagePropertyProvider.php.
NeoWikiExtension::getInstance() exposes read-side services usable from any MediaWiki extension point
(hooks, special pages):
newSubjectAuthorizer( Authority )— subject permission checks.newPageSubjectsLookup()— look up the subjects on a page.newSubjectContentRepository()— read Subject data by id.newFrontendModuleLoader()— mount NeoWiki's UI on any page.
Examples: src/RedHerbSidebarHook.php
and src/Specials/SpecialRedHerbSubjectFinder.php.
NeoWiki's frontend is built with TypeScript and Vue. Extensions consume it as plain JavaScript and need no build step. You can also author in TypeScript with types; see "Authoring in TypeScript" below.
Getting your JavaScript onto NeoWiki pages takes two steps. First, declare a ResourceLoader module that depends
on ext.neowiki, which makes require( 'ext.neowiki' ) available:
"ResourceModules": {
"ext.myext": {
"class": "MediaWiki\\ResourceLoader\\CodexModule",
"dependencies": [ "vue", "ext.neowiki" ],
"packageFiles": [ "init.js" ]
}
}Then load that module alongside NeoWiki's UI by handling the NeoWikiGetFrontendModules hook:
class MyExtFrontendModulesHook implements NeoWikiGetFrontendModulesHook {
public function onNeoWikiGetFrontendModules( array &$modules, OutputPage $out, Skin $skin ): void {
$modules[] = 'ext.myext';
}
}Example: src/RedHerbFrontendModulesHook.php.
A backend Property Type needs a matching frontend: a display component, an input component, and an
attributes editor. Register them through the neowiki.registration JS hook:
const nw = require( 'ext.neowiki' );
mw.hook( 'neowiki.registration' ).add( ( registrar ) => {
registrar.registerPropertyType( {
typeName: 'color',
valueType: nw.ValueType.String,
displayAttributeNames: [],
createPropertyDefinitionFromJson: function ( base, json ) {
return Object.assign( {}, base, {
allowedColors: Array.isArray( json.allowedColors ) ? json.allowedColors : []
} );
},
getExampleValue: function () {
return nw.newStringValue( '#ff5733' );
},
validate: validate,
displayComponent: ColorDisplay,
inputComponent: ColorInput,
attributesEditor: ColorAttributesEditor,
label: 'myext-property-type-color',
icon: icons.cdxIconHighlight
} );
} );The registration object's shape is defined by
PropertyTypeRegistration.ts;
every field is required, including attributesEditor even for a type with no configurable attributes. The
typeName must equal the backend PropertyType::getTypeName(). The display, input, and attributes-editor
components conform to NeoWiki's component prop shapes — see
ValueDisplayContract.ts,
ValueInputContract.ts,
and AttributesEditorContract.ts.
Full example: resources/init.js
with ColorDisplay.vue,
ColorInput.vue,
and ColorAttributesEditor.vue.
A View Type renders a Subject in a particular visual format; infobox is the only built-in one. Register a Vue
component for a new View Type through the same neowiki.registration hook, at parity with Property Types:
const nw = require( 'ext.neowiki' );
const RedHerbCard = require( './RedHerbCard.vue' );
mw.hook( 'neowiki.registration' ).add( ( registrar ) => {
registrar.registerViewType( {
typeName: 'redherb-card',
component: RedHerbCard
} );
} );The registration object's shape is defined by
ViewTypeRegistration.ts:
a typeName and the Vue component that renders it. The component conforms to the ViewProps prop shape
(ViewContract.ts):
the subjectId to render, a canEditSubject flag, and an optional layoutName. Resolve any Layout-specific
configuration (Display Rules and Settings) from the layout store using layoutName. Once registered, the
typeName becomes selectable as a Layout's View Type, and a {{#view}} (or Main Subject) placeholder that
references it renders through your component instead of the built-in infobox.
The redherb-card example reuses NeoWiki's own building blocks rather than rendering values by hand: the subject,
schema, and layout stores; nw.resolveDisplayProperties together with the value-display component registry to
render each value through its Property Type's component; and the shared nw.SubjectEditorDialog for editing when
canEditSubject is true.
Full example: resources/init.js
with RedHerbCard.vue.
require( 'ext.neowiki' ) returns NeoWiki's public API barrel; its exports are listed in
public-api.ts.
The value model and factories (newStringValue, newNumberValue) live in
domain/Value.ts;
value shape varies by valueType.
To build a Vue feature wired to NeoWiki's services, obtain NeoWiki's Pinia instance and register its services on your app:
const nw = require( 'ext.neowiki' );
const app = Vue.createMwApp( MyComponent );
app.use( nw.NeoWikiExtension.getInstance().getPinia() );
nw.NeoWikiServices.registerServices( app );
app.mount( '#my-mount-point' );Examples: resources/createChild/,
resources/editMainSubject/,
and resources/subjectFinder/.
Plain JavaScript is the simplest path and needs no build step. You can instead write your extension in TypeScript and get types for NeoWiki's API. This is configuration on your side; NeoWiki ships nothing extra for it. See ADR 24 for the reasoning.
Point your tsconfig.json paths at NeoWiki's barrel source, which sits next to your extension in extensions/:
"paths": {
"ext.neowiki": [ "../NeoWiki/resources/ext.neowiki/src/public-api" ]
}You then get types on the same specifier you load at runtime, for example
import { ValueType, newStringValue } from 'ext.neowiki'; and
import type { PropertyTypeRegistration } from 'ext.neowiki';. Mark the modules NeoWiki already provides as external
in your bundler, so you do not ship a second copy and break the shared store: ext.neowiki, vue, @wikimedia/codex,
@wikimedia/codex-icons and pinia. At runtime your built JavaScript loads the same ext.neowiki module as the rest
of the page.
A Property Type validates in two places: the backend PropertyType::validate() (authoritative — returns
Violation[]) and the frontend validate (immediate UX feedback — returns an array of { code } objects,
empty meaning valid). Implement both. NeoWiki resolves each frontend code as the message key
neowiki-field-<code>; your extension must define those messages. For example, returning
{ code: 'invalid-hex' } requires a neowiki-field-invalid-hex message (see RedHerb's
i18n/en.json).
The frontend registration's icon is a Codex Icon. RedHerb uses stock Codex icons (browse the
icon gallery) declared via
CodexModule::getIcons in its
extension.json.
Custom SVG icons are also supported — pass an SVG string as the icon.
These extension points are designed or partially present but not yet open to extensions:
- Graph database backends. A
GraphDatabasePlugininterface exists, but Neo4j is the only backend and is currently hardcoded. - A published TypeScript types package. TypeScript authors get types today by pointing their
tsconfigat NeoWiki's source (see "Authoring in TypeScript" and ADR 24). A published, versioned package is deferred until a consumer needs types without a NeoWiki checkout.