Publish a news item with a lead image

Publish a news item with a lead image#

This guide shows you how to create a news item with blocks and a lead image, and publish it.

Tip

create_with_files() does all of this in one call, and picks the lead image field from the schema; see Create content in one step. This guide shows the steps it takes.

Build the blocks#

plone_client.blocks builds the blocks and blocks_layout fields of a Volto page.

from plone_client import blocks

data = {
    "title": "Plone 6.2 is out",
    "description": "The release brings a new distribution.",
    **blocks.build(
        blocks.title(),
        blocks.description(),
        blocks.text("Plone 6.2 is out."),
    ),
}
item = client.content.create("/news", "News Item", data)

Attach the lead image#

Sites differ in where a news item keeps its lead image. plone.restapi ignores fields that a content type does not have, so a wrong guess creates the item without an image and reports no error. Read the content type’s schema to know which field to set.

fields = client.types.get("News Item")["properties"]

If the schema has an image field, as Classic UI sites do, send the image with the item.

from plone_client import file_data

data["image"] = file_data("lead.jpg")

If it has preview_image_link instead, as Plone 6.1 sites created for Volto do, upload the image inside the item and link to it by UID.

lead = client.files.upload(item["@id"], "lead.jpg")
client.content.update(item["@id"], preview_image_link=lead["UID"])

Publish it#

client.workflow.transition(item["@id"], "publish")

To publish at a future date, pass effective="2026-11-01T09:00:00".

The complete script#

The script reads the title, the description, and the paragraphs from a text file. Without --apply it only prints the request it would send.

python publish_news.py news.txt /news --image lead.jpg
python publish_news.py news.txt /news --image lead.jpg --apply
"""Publish a news item, with a lead image, from a text file.

The first line of the text file is the title, the second the description,
and each later paragraph becomes a text block. Without ``--apply``, the
script only reports what it would send.

Sites differ in how a news item holds its lead image: Plone 6.1 sites
created for Volto link to an image with ``preview_image_link``, while others
store it in an ``image`` field. The script reads the content type's schema
and does whichever the site expects.

Usage::

    python docs/_examples/publish_news.py news.txt /news --image lead.jpg
    python docs/_examples/publish_news.py news.txt /news --image lead.jpg --apply
"""

from pathlib import Path
from plone_client import blocks
from plone_client import file_data
from plone_client import PloneClient
from plone_client import PloneClientError
from typing import Any

import argparse
import json
import sys


def news_data(source: Path) -> dict[str, Any]:
    """Field values of the news item.

    :param source: Text file: title, description, then paragraphs.
    :returns: The fields, blocks included.
    """
    title, description, *rest = source.read_text().split("\n")
    paragraphs = [p.strip() for p in "\n".join(rest).split("\n\n") if p.strip()]
    return {
        "title": title.strip(),
        "description": description.strip(),
        **blocks.build(
            blocks.title(),
            blocks.description(),
            *(blocks.text(paragraph) for paragraph in paragraphs),
        ),
    }


def publish(
    client: PloneClient, folder: str, data: dict[str, Any], image: Path | None
) -> dict[str, Any]:
    """Create the news item, attach its lead image, and publish it.

    :param client: Client of the site.
    :param folder: Path of the folder.
    :param data: Field values of the news item.
    :param image: Lead image, if any.
    :returns: The news item, or the skipped request in a dry run.
    """
    fields = client.types.get("News Item")["properties"]
    if image and "image" in fields:
        data = {**data, "image": file_data(image)}
    item = client.content.create(folder, "News Item", data)
    if client.dry_run:
        # Nothing was created, so there is nothing to attach or publish yet.
        return item
    if image and "image" not in fields and "preview_image_link" in fields:
        lead = client.files.upload(item["@id"], image)
        client.content.update(
            item["@id"], preview_image_link=lead["UID"], representation=False
        )
    client.workflow.transition(item["@id"], "publish")
    return item


def main() -> int:
    """Run the script.

    :returns: The exit code.
    """
    parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
    parser.add_argument("source", type=Path, help="Text file with the news")
    parser.add_argument("folder", help="Path of the folder in the site")
    parser.add_argument("--image", type=Path, help="Lead image")
    parser.add_argument("--profile", help="Profile to use, instead of $PLONE_CLIENT_*")
    parser.add_argument("--apply", action="store_true", help="Change the site")
    args = parser.parse_args()

    settings = {"dry_run": not args.apply}
    try:
        if args.profile:
            client = PloneClient.from_profile(args.profile, **settings)
        else:
            client = PloneClient.from_env(**settings)
        with client:
            item = publish(client, args.folder, news_data(args.source), args.image)
            log = client.dry_run_log
    except PloneClientError as error:
        print(f"Error: {error}", file=sys.stderr)
        return 1
    if not args.apply:
        for entry in log:
            entry.get("json", {}).get("image", {}).pop("data", None)
            print(json.dumps(entry, indent=2, ensure_ascii=False))
        print("Dry run: nothing was changed")
        return 0
    print(f"Published {item['@id']}")
    return 0


if __name__ == "__main__":
    sys.exit(main())