This project is not covered by Drupal’s security advisory policy.
Drupal knows what a node references directly, but not what those references reference in turn. Content Graph keeps a projection of every content entity on your site and every reference between them.
That lets you ask what a page depends on and get the complete answer: paragraphs inside paragraphs, media and their files, terms, blocks, Layout Builder, Canvas and Site Studio components, all the way down.
What is a "content graph"?
Drupal already stores relationships, and Views can follow them. A content graph makes those relationships easier to query across the whole site, especially through several connections.
For example, Views is well suited to showing all content written by a particular author, or showing content tagged with items that other content has tagged.
You can also configure relationships to answer more involved questions. But consider a scenario like display content connected to this author through their department, research topics, collaborators, or events.
With Views, you generally need to know those routes in advance and configure the relevant relationships and queries. As routes multiply, that becomes harder to manage.
A content graph represents those connections consistently: this entity -> this relationship -> that entity, regardless of which entity type or field originally supplied the connection. That gives you several benefits:
- Related content: Follow several kinds of connections instead of matching only a shared taxonomy term.
- Impact analysis: Trace which content references or depends on an entity, including indirect connections.
- Content auditing: Find disconnected content, clusters, and gaps across entity types.
- Search and AI: Retrieve surrounding context and explain how one piece of content relates to another.
- Exploring relationships: Ask “How are these two items connected?” without configuring every possible route beforehand.
Other resources to read:
- https://hygraph.com/blog/the-content-graph-is-the-future
- https://www.pwc.com/us/en/services/consulting/commercial-excellence/content-knowledge-graph-ai-content-personalization.html
- https://www.optimizely.com/field-notes/articles/content-graph-announcement
This is possible from adding Common Table Expression (CTE) support. Drupal's database API can't express a recursive query: there's no WITH RECURSIVE in core's query builder (yet). The module adds a small builder of its own, CommonTableExpressionQuery that composes the query from three parts:
- An anchor: the root entities.
- A recursive step: join the edge table to the entities reached so far, one hop deeper, up to a depth limit, following only default-revision edges.
- An outer SELECT: dedupe what was reached and keep each entity's shortest depth.
Values are bound, never interpolated. It runs on every database Drupal 11 supports: MySQL 8, MariaDB, PostgreSQL and SQLite.
WITH RECURSIVE reachable (entity_type, uuid, depth) AS (
SELECT ... FROM content_graph_entity WHERE <roots> -- anchor
UNION ALL
SELECT child... FROM content_graph_edge JOIN reachable ... -- one hop deeper
WHERE depth < :max_depth AND <default revision edge>
)
SELECT entity_type, uuid, MIN(depth) FROM reachable GROUP BY entity_type, uuid
They back bulk exports, which can cover the whole site. For one or a few roots, the PHP walk does the same traversal and also records the edges, the missing targets and any cycles it hits.
Why this works:
- One query, any depth. "Everything this depends on, however deep" is a single round trip to the database. Without the CTE, you load an entity, read its reference fields, load those, and repeat. That's what Entity Share does over HTTP and what custom code does with entity loads.
- Every kind of reference in one place. Plain entity references, revision references, files, links, rich text embeds, Layout Builder inline blocks and Canvas inputs all become rows in one edge table. The query walks one table. It doesn't need a resolver for each field type.
- Reverse lookups for free. The same edges walked backwards answer "what uses this?". Live Drupal can only answer that by querying every field that might point at the entity.
- No extra infrastructure. Neo4j or ArangoDB would give you graph traversal, but as a separate service to run and keep in sync. The CTE gets the core traversal out of the database you already have. The mirrors stay optional, for when you want a real graph engine.
- Exact by construction. The index is updated during each save, and the query follows only current-revision edges. So the answer matches the site's published state, not a stale export or a guess.
One practical use case is included with this module - pushing content over the wire to another environment. Pick a content entity, like node, and Content Graph packages the current published version of it and everything it depends on into a single zip. Another environment can validate, preflight, dry-run and apply that zip in one transaction.
Those features are very experimental and can have edge cases. But on a handful of real projects, the module has successfully pushed entity changes, including data in Layout Builder, Paragraphs, Site Studio, and Canvas. This type of functionality has long been asked of Drupal and is similar to what you'd see in Optimizely, Sanity, and others.
Features
- Full dependency closure of any entity, through entity references, revision references, media, files, rich text embeds, links, Layout Builder, Paragraphs, Canvas and Site Studio.
- Promotion packages: checksummed zips with content-addressed file bytes. Import them with Drush or the admin UI, with a preflight that stops early on missing modules, bundles or fields.
- Safe imports: a dry-run plan per entity, conflict detection when the target has changed since the last import, and an all-or-nothing apply.
- Authorship is preserved. Owners and user references travel by UUID, so "edit own content" still works for the right person on the target. Unknown accounts arrive as blocked stubs. Passwords, email addresses and roles are never packaged.
- Access-aware. The sender must be able to view everything a package would carry.
- Deletions are promoted as Trash state, recoverable, not destructive.
- A page-builder inventory of Layout Builder, Paragraphs, Site Studio and Canvas usage.
- Optional mirrors of the graph to Neo4j or ArangoDB.
- Experimental: pushing packages to a remote site from the UI, with a dispatch log and retries, and a read-only JSON API over the graph.
Post-installation
Run drush content-graph:rebuild once to index existing content. After that, every save keeps the graph current automatically. Export with drush cgxp node:UUID --output=/tmp/package.zip and import on the target with drush cgip (dry run) and drush cgia (apply), or use the admin UI under Content.
Additional requirements
Drupal 11.2 or later, or Drupal 12. The main module needs nothing beyond core.
Integrations are separate submodules, each needing its own contrib module or library.
Recommended modules
- Paragraphs, Canvas and Site Studio, for their integrations.
- Trash, for deletion promotion and the remote modules.
- Key, for storing remote destination passwords.
- Views Bulk Operations, for packaging from a view.
Similar projects
- Migrate (core): for moving content between different schemas, upgrades and external sources, with transformation and rollback. Content Graph assumes both sites share configuration and moves content as is.
- Entity Share: for ongoing, pull-based content sharing between independent sites over JSON:API. It finds references during the import, one request at a time, and doesn't carry authorship.
- Workspaces (core): for staging content changes within one site, not between environments.
Project information
- Project categories: Artificial Intelligence (AI), Import and export, Site search
- Created by kevinquillen on , updated
This project is not covered by the security advisory policy.
Use at your own risk! It may have publicly disclosed vulnerabilities.
