Problem/Motivation

When downloading content as Markdown, both via the "Content First Download Markdown ZIP" bulk action and the individual download button on the content-first node view, the generated filename is derived from the node title. This produces filenames that do not reflect the actual URL structure of the content and can be ambiguous when titles are similar or contain characters that differ across languages.

Additionally, both code paths duplicated the filename generation logic independently.

A secondary bug exists in ContentFirstDownloadController::downloadZip(): the validation regex [a-zA-Z0-9]+ does not match the output of Crypt::randomBytesBase64(), which produces base64url-encoded strings containing - and _ characters. This causes the download to return a 404 approximately 30% of the time.

Proposed resolution

Introduce a ContentFirstFilename service that centralises filename generation. The service:

Resolves the node's path alias for its language via path_alias.manager.
Reads the configured URL language prefix from language.negotiation config and prepends it when present (e.g. en_about-us).
Detects the site front page by comparing against system.site → page.front (resolved through the alias manager to handle cases where the setting stores an alias rather than an internal path) and returns index or {lang}_index accordingly.
Falls back to the internal path (node/{id}) when no alias is configured.
Replaces all forward slashes with underscores for OS compatibility.
Returns the filename without extension so callers can append .md, .zip entry names, or .txt as appropriate.
Inject ContentFirstFilename into EntityController (for use in NodeController) and into ContentFirstDownloadZip.

Fix the variable collision in ContentFirstDownloadZip::executeMultiple() where $filename was reused for both the ZIP archive name and the per-entry filename, causing $store->set('download_zip', $filename) to store the node path instead of the archive name.

Fix the validation regex in ContentFirstDownloadController::downloadZip() to accept - and _ characters produced by Crypt::randomBytesBase64().

Example filenames produced

Scenario Result
Node with alias /about-us, lang prefix en en_about-us.md
Node with alias /services/consulting, lang prefix en en_services_consulting.md
Node with no alias, lang prefix en en_node_23.md
Front page node, lang prefix en en_index.md
Front page node, no lang prefix index.md

User interface changes

None. Only the generated download filename changes.

API changes

New service content_first.filename_helper (Drupal\content_first\ContentFirstFilename) with one public method getFilename(NodeInterface $node): string.

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

gedur created an issue. See original summary.

gedur’s picture

Assigned: gedur » Unassigned
Status: Active » Needs review

Please review and merge

eduardo morales alberti made their first commit to this issue’s fork.

eduardo morales alberti’s picture

Waiting for tests

eduardo morales alberti’s picture

Status: Needs review » Fixed

Fixed!

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.