protonsystems / demo-content
Reusable deterministic demo-content generation for paragraph-heavy Drupal projects.
Package info
gitlab.com/Proton.Systems/drupal/demo-content
Type:drupal-module
pkg:composer/protonsystems/demo-content
Requires
- php: ^8.3
- drupal/core: ^10 || ^11
- drupal/paragraphs: ^1.20
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Reusable Drupal module for generating deterministic demo content on paragraph-heavy sites.
Purpose
The module is intended to be reused across Drupal 10 and Drupal 11 projects by installing it as a Composer package and enabling it in the target site.
What it does
- creates a deterministic node for a chosen content type;
- discovers the main paragraph field automatically;
- creates one instance of every allowed top-level paragraph bundle;
- recurses into nested paragraph fields;
- creates or reuses referenced media, nodes, terms, and other entities;
- skips problematic computed/internal fields such as node
menu_link; - creates the site's standard boilerplate pages (Home, Privacy Policy, Legal Notice, Contact) with real placeholder copy, a recommended URL alias, and Home set as the front page;
- builds and verifies a real homepage from a small YAML spec, with deterministic, single-line JSON output that an AI agent (or a script) can drive with almost no token cost;
- tracks every entity it generates so it can all be deleted again with one command.
Requirements
- PHP 8.3+
- Drupal 10 or 11
- Paragraphs
- Drush 12+
Commands
drush demo-content:coverage-node <bundle> (alias: demo:coverage-node)
Creates one coverage node for the requested node bundle. The command requires a paragraph field on that bundle, either auto-detected or supplied explicitly. It creates one top-level instance of every allowed paragraph bundle and then recurses into nested paragraph/reference fields.
drush demo-content:coverage-node page
drush demo-content:coverage-node page --replace
Options:
--title=<text>: use a custom node title instead of the generated default--paragraph-field=<machine_name>: override paragraph field auto-detection--nested-repeat=<n>: repeat single-bundle nested paragraph/reference lists; defaults to2--replace: delete an existing node with the same bundle/title before creating the new coverage node
Examples:
drush demo-content:coverage-node page --title="Paragraph coverage page"
drush demo-content:coverage-node page --paragraph-field=field_paragraphs
drush demo-content:coverage-node page --nested-repeat=3
drush demo-content:coverage-node page --replace
drush demo-content:coverage-all (alias: demo:coverage-all)
Creates one demo node for every requested node bundle. If --bundles is not
provided, the module runs across all available node bundles in stable sorted
order. Bundles without a paragraph field are still created; those nodes are
logged as created without paragraph coverage.
drush demo-content:coverage-all
drush demo-content:coverage-all --bundles=page,project --replace
Options:
--bundles=<bundle1,bundle2>: comma-separated node bundles; defaults to all node bundles--nested-repeat=<n>: repeat single-bundle nested paragraph/reference lists; defaults to2--replace: delete an existing generated node with the same bundle/title before recreating it
Examples:
drush demo-content:coverage-all --bundles=page,project
drush demo-content:coverage-all --bundles=page,project --nested-repeat=3 --replace
drush demo-content:create-standard-pages (alias: demo:standard-pages)
Creates the site's standard boilerplate pages — Home, Privacy Policy, Legal
Notice, Contact — as page nodes (or another bundle via --bundle), each
with real placeholder copy (not synthetic coverage data), a recommended URL
alias, and Home set as the site's front page.
drush demo-content:create-standard-pages
drush demo-content:create-standard-pages --pages=privacy_policy,legal_notice
drush demo-content:create-standard-pages --replace
Recommended/default URLs: /home (also set as front page), /privacy-policy,
/legal-notice, /contact.
Options:
--bundle=<machine_name>: node bundle to use; defaults topage--pages=<id1,id2>: comma-separated subset ofhome,privacy_policy,legal_notice,contact; defaults to all four--replace: delete an existing page with the same title before creating it--no-front: don't set Home as the site front page
Running it again reuses pages that already exist (matched by title) instead of
creating duplicates; use --replace to recreate them. These pages carry
placeholder copy, so demo-content:check-homepage fails them until replaced —
use demo-content:homepage for a real homepage.
Node IDs are never forced — Drupal assigns them automatically. On a fresh install with an empty node table, Home (created first) naturally becomes node 1.
Language: pages are currently created in English only. A --langcode
option (so a German-default site can get Impressum instead of Legal
Notice, for example) is planned but not yet implemented — see
STANDARD_PAGES in DemoContentGenerator for the extension point.
drush demo-content:check-homepage (alias: demo:check-home)
Checks the site's front page and prints one line of JSON; the exit code is
1 when any check fails, so a caller can stop reading at "ok":true.
drush demo-content:check-homepage
drush demo-content:check-homepage --http --pretty
{"ok":false,"fail":1,"warn":0,"pass":12,"homepage":{"nid":36,"bundle":"page","title":"Home","alias":"/home","langcode":"en","sections":["hero","text"]},"issues":[{"id":"placeholder_copy","level":"fail","msg":"Placeholder text in: node.body"}]}
Only issues are listed (--verbose-checks also lists what passed). Checks:
front_page (resolves to a node), published, language (matches the site
default), alias, content (paragraphs or body), validation (entity
constraints, run as user 1), placeholder_copy (lorem ipsum, "placeholder",
"replace this", …), empty_sections, links (broken internal links),
translations, duplicates, generated (homepage is still sample content),
and with --http a real GET (status 200, exactly one <h1>).
drush demo-content:check-page <page> and drush demo-content:check-standard-pages
check-page runs the same checks as check-homepage on any single page, given
a node ID or URL alias. Extra rules are opt-in, suited to legal pages:
drush demo-content:check-page /privacy-policy --min-text=200 --menu
--min-text=<n>: fail when the page has fewer thanncharacters of text (a legal page of two sentences is not launch-ready);--menu: warn when no menu link points to the page (footer menu, etc.).
An alias that resolves to something other than a node (for example a webform
at /contact) is checked as a route: it passes when the path is valid.
check-standard-pages checks Home, Privacy Policy (/privacy-policy), Legal
Notice (/legal-notice) and Contact (/contact) in one call and prints one
line of JSON: {"ok":…,"pages":{"privacy_policy":{"ok":…,"fail":…,"warn":…,"nid":…,"issues":[…]},…}}.
Privacy Policy and Legal Notice are checked with --min-text=200 --menu.
drush demo-content:homepage-schema (alias: demo:home-schema)
Prints, as one line of JSON, what a homepage spec may contain for this site's
paragraph model: allowed section bundles, their fields, which are required
(*), and value kinds. This replaces reading configuration files.
drush demo-content:homepage (alias: demo:home)
Builds or rebuilds the homepage from a YAML/JSON spec. Idempotent: it updates the same node in place (no duplicates, no orphaned paragraphs), sets the alias and makes it the front page.
title: Home
alias: /home
langcode: en
sections:
- type: hero
slides:
- title: Welcome
description: What we do, in one sentence.
link: {url: /contact, title: Contact us}
media: Alt text for a generated placeholder image
- type: text
text: |
First paragraph.
Second paragraph.
drush demo-content:homepage --spec=docs/content/homepage.yml --dry-run
drush demo-content:homepage --spec=docs/content/homepage.yml
drush demo-content:homepage # sample homepage from the site's own bundles
drush demo-content:homepage --print-default-spec # show that sample spec
--dry-run validates the spec and returns every error at once, each naming
the valid alternatives, so a fix loop costs one round trip:
{"ok":false,"dry_run":true,"errors":["sections[0] (hero).slides[0] (hero_slide): unknown field \"colour\". Valid: description, link, media, title."]}
The same command builds any other page: add --no-front, and give the spec a
body (or body_file: text.html, read relative to the spec, to keep long
client-supplied text out of the YAML) instead of sections. An existing page
with the same alias (or title) is updated in place.
Spec paths resolve against the current directory, then the project root. Field
keys are field machine names without field_. Value kinds: string, text
(HTML, or plain text where a blank line starts a new paragraph), link
(/path, a full URL, or {url, title}), image (alt text for a generated
placeholder, or {file, alt}), and nested paragraph lists. Fields that
reference other entities (media, nodes, terms) cannot be set from a spec yet.
A homepage built from a spec is treated as real content and is not tracked,
so demo-content:purge never deletes it. Only the sample homepage (no spec) is
tracked as generated.
drush demo-content:navigation (alias: demo:nav)
Builds menus and menu links from a YAML spec. Menu links are content
entities, so drush cex never exports them; keeping the spec in the project
repository gives navigation a reproducible source that any environment can
re-apply. Idempotent: every link gets a UUID derived from its menu and id,
so a rerun updates in place instead of creating duplicates.
menus:
legal: # created when it does not exist
label: Legal
description: Legal notice and privacy links
links:
- id: legal-notice # stable key, unique within the menu
title: Legal notice
link: /legal-notice # <front>, /alias, route:route.name, https://...
weight: 0 # optional; also expanded, enabled, description
translations: {es: Aviso legal}
children: [] # optional, same shape
disable: # optional: module-provided links to hide
- views_view:views.tasks.page_1
drush demo-content:navigation --spec=docs/content/navigation.yml --dry-run
drush demo-content:navigation --spec=docs/content/navigation.yml
--dry-run validates without writing and returns every error at once: unknown
keys, duplicate ids, links that do not resolve, languages that are not
enabled, and translations requested while menu links are not translatable.
Aliases that point at a node are stored as entity:node/N, so the link
follows the node and gets the right language prefix. Output is one line of
JSON with created/updated/unchanged counts per menu; the exit code is 1 on
any error.
drush demo-content:purge (alias: demo:purge)
Deletes every entity this module has generated — across every past command
invocation, not just the most recent one. This covers top-level nodes,
nested paragraphs, and any referenced media/taxonomy/nodes/other entities
created along the way, plus any path aliases created by
demo-content:create-standard-pages.
drush demo-content:purge
drush demo-content:purge --dry-run
Options:
--dry-run: report what would be deleted without deleting anything--no-legacy-scan: skip the title-pattern sweep described below
Generated entities are tracked in Drupal State as they're created, so purge
doesn't need to guess what's demo content — with one exception: content
created by a module version before this tracking existed has no State
record. To handle that, purge also sweeps for nodes titled with the
Demo coverage: prefix that demo-content:coverage-node/coverage-all use
by default (not the paragraphs/media/other entities those runs also created,
since only that node title prefix is distinctive enough to be a safe,
generic content signal). A node created with a custom --title bypasses
this sweep; going forward, any run of this module tracks entities exactly,
regardless of title.
Installation
composer require protonsystems/demo-content
drush en demo_content
Extension points
The generic engine lives in a service, while project-specific customizations
can be added via alter and presave hooks documented in demo_content.api.php.
Releasing A New Version On Packagist
Packagist publishes versions of protonsystems/demo-content from git tags. Do
not add a version field to composer.json; create and push a new release tag
instead.
For the first public release, submit the public Git repository URL to
Packagist. If you want automatic updates from GitLab, configure the Packagist
integration in GitLab under Settings > Integrations using your Packagist
username and API token.
Prepare the release commit on main:
git checkout main
git pull origin main
composer validate --no-check-publish --strict
git status
git add <files>
git commit -m "Release v1.0.0"
git push origin main
Create and push an annotated tag for the release:
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0
Use semantic version tags such as v1.0.0. After the tag is pushed, Packagist
should detect the new version automatically. If it does not appear, open the
package page in Packagist and trigger a manual update.