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.
Issue fork content_first-3588311
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
Comment #3
gedur commentedPlease review and merge
Comment #5
eduardo morales albertiWaiting for tests
Comment #7
eduardo morales albertiFixed!