ninorai / ninorai-drupal
Receives AI-generated SEO articles from the NinorAI pipeline — via the REST/OAuth2 API or by importing an article bundle (.zip) — and stores them as ninorai nodes.
Package info
github.com/Ninorai-Ai/ninorai-drupal
Type:drupal-module
pkg:composer/ninorai/ninorai-drupal
Requires
- php: >=8.1
- drupal/consumers: ^1.17
- drupal/core: ^10 || ^11
- drupal/simple_oauth: ^5.4 || ^6
README
Receives AI-generated SEO articles from the NinorAI
pipeline and stores them as ninorai nodes. It is the Drupal counterpart of
the NinorAI WordPress plugin.
There are two ways to get an article into Drupal:
- API push — NinorAI authenticates (OAuth2 bearer token, or HTTP Basic Auth
as a fallback) and posts the article as JSON, or uploads the whole bundle
.zipin one request. - Manual bundle import — an editor downloads the bundle
.zipfrom NinorAI and uploads it on an admin page. No connection or credentials needed.
Either way the article is created as an unpublished draft so an editor (or the scheduled-publish cron) takes it live.
Requirements
- Drupal 10 or 11.
- Core:
node,taxonomy,file,image,path,rest,basic_auth,serialization,datetime. - Contrib (for OAuth2):
simple_oauthandconsumers— pulled in automatically by composer.
Installation
composer require ninorai/ninorai-drupal drush en ninorai
If the package is not yet visible on Packagist for you, add the repository
directly to your project's composer.json first:
"repositories": [ { "type": "vcs", "url": "https://github.com/Ninorai-Ai/ninorai-drupal" } ]
Authentication setup
Open Configuration → Web services → NinorAI integration
(/admin/config/services/ninorai).
OAuth2 (recommended)
- First click Create user and generate password to create the
ninoraiservice user (the OAuth client acts as this user, inheriting only the NinorAI service role's permissions). - Configure OAuth signing keys once at Configuration → People → Simple OAuth
(
drush simple-oauth:generate-keysor the on-page generator). - Click Generate OAuth client. Copy the Client ID and Client secret (the secret is shown only once) into NinorAI's site settings.
NinorAI exchanges the credentials at the token endpoint using the
client_credentials grant:
POST /oauth/token
grant_type=client_credentials&client_id=<id>&client_secret=<secret>
and sends the returned token as Authorization: Bearer <token> on every request.
HTTP Basic Auth (fallback)
Click Create user and generate password and paste the username + password
into NinorAI. Endpoints accept both auth methods (_auth: ['oauth2', 'basic_auth', 'cookie']).
Endpoints
| Method | Path | Purpose |
|---|---|---|
POST |
/api/ninorai/articles |
Create an article from JSON. |
PATCH |
/api/ninorai/articles/{ninorai_id} |
Update an article by NinorAI ID. |
GET |
/api/ninorai/articles/{ninorai_id} |
Read one article. |
GET |
/api/ninorai/articles?page=N&per_page=M |
Paginated list (Site Sync read-back). |
POST |
/api/ninorai/articles/bundle |
Upload a whole bundle .zip ("just the zip"). |
The bundle endpoint accepts either multipart/form-data with a bundle file
part, or a raw application/zip request body.
Manual import
Content → Import NinorAI bundle (/admin/content/ninorai/import) — upload a
.zip and a draft article is created with its images sideloaded into the
managed file store and the body rewritten to use them, so the article is
self-contained.
The bundle format
A NinorAI bundle is the artefact the platform builds at
GET /api/v1/articles/{id}/bundle. It is a .zip containing:
manifest.json—title,slug,meta_title,meta_description,focus_keyword,language,categories[],featured_image(relative path),featured_image_alt,content_file, andschema(JSON-LD).article.html— the post body, with images referenced as relativeimages/...paths.images/— the featured and inline image files.
Entries are read straight out of the archive and every name is basenamed, so a
crafted bundle cannot traverse outside its intended targets (zip-slip safe).
Imports are deduplicated by ninorai_id (the slug) — re-importing the same
bundle updates the existing node (as a new revision) instead of creating a
duplicate. A new import lands as a draft; an update never changes the
publish state, so an article an editor has already taken live stays live.
Stored JSON-LD schema
When a ninorai is viewed in full, its stored schema is emitted into
<head> as <script type="application/ld+json">. The JSON is re-encoded with
JSON_HEX_TAG so it is safe to print inside a script tag.
Tests
# From the Drupal root:
vendor/bin/phpunit -c core web/modules/custom/ninorai/tests
tests/src/Unit/Bundle/BundleArchiveTest.php covers bundle parsing and the
zip-slip protection with no Drupal bootstrap. Integration tests for the importer
and endpoints run inside a Drupal install.