Search by

protonsystems / demo-content

proton.systems

Reusable deterministic demo-content generation for paragraph-heavy Drupal projects.

Package info

gitlab.com/Proton.Systems/drupal/demo-content

Issues

Type:drupal-module

pkg:composer/protonsystems/demo-content

Statistics

Installs: 26

Dependents: 0

Suggesters: 0

Stars: 0

v1.4.0 2026-09-25 22:11 UTC

This package is auto-updated.

Last update: 2026-09-26 03:23:03 UTC


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 to 2
  • --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 to 2
  • --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 to page
  • --pages=<id1,id2>: comma-separated subset of home, 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 than n characters 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.