>>
> >> >  etc/ORG-NEWS                 |  21 +++
> >> >  lisp/ox-html.el              |  55 +++++---
> >> >  testing/lisp/test-ox-html.el | 241 +++++++++++++++++++++++++++++++++++
> >>
> >> I think we should also document the new option in the manual.
> >>
> >
> > Done.
>
> Also, update the commit message.
>

I updated the commit message. Are manual updates supposed to go into
the commit message?

> >> Mutating INFO can cause unintended side effects.
> >> Rather than trying to change INFO + calling
> >> org-publish-file-relative-name, we should factor out
> >> org-publish-file-relative-name into a more global function (in ox.el)
> >> and use it from ox-html and ox-publish.
> >> I imagine that function to look like
> >>
> >> (org-export-file-relative-name-maybe filename base-directory)
> >>
> >> > +         (file-relative-name (org-publish-file-relative-name
> >> > +                              raw-path updated-info))
> >> > +         (home (and (plist-get info :html-link-home)
> >> > +                    (org-trim (plist-get info :html-link-home)))))
> >> > +    ;; Possibly append `:html-link-home' to relative file
> >> > +    ;; name.
> >>
> >
> > Done.
>
> Any reason you went with org-publish-*?
>

Fixed. My bad, I misread your suggestion.

> > +#+vindex: org-html-base-directory
> > +~org-html-base-directory~ can be used to set the directory of
> > +the project that you intend to export. When used in conjunction
> > +with ~org-html-link-home~ and ~org-html-link-use-abs-url~, file
> > +links to files in ~org-html-base-directory~ will have the value
> > +of ~org-html-link-home~ prepended to them.  For example, when
> > +~org-html-base-directory~ is set to =~/my/project=, ~org-html-link-home~
> > +is set to =https://orgmode.org= and ~org-html-link-use-abs-url~ is set to 
> > ~t~,
> > +~[[~/my/project/contribute][Contributing to Org]]~ will be exported as
>
> I just realized that here an elsewhere you document this as a custom
> option, while it is export option, that can be set both via variable and
> also in-buffer keyword.
>

I made this clear by documenting the in-buffer keyword in ORG-NEWS
and the manual.

> > +(defcustom org-html-base-directory nil
> > +  "Directory of the project that you intend to export.
> > +This variable is the equivalent of the `:base-directory'
> > +key of `org-publish-project-alist'.  If `:base-directory'
> > +is also set, the value of this variable will be ignored."
> > +  :group 'org-export-html
> > +  :package-version '(Org . "10.0")
> > +  :safe #'stringp
>
> string-or-null-p
>

Done.

> > +  :type 'string)
>
> Or nil.
>

Done.

> --
> Ihor Radchenko // yantar92,
> Org mode maintainer,
> Learn more about Org mode at <https://orgmode.org/>.
> Support Org development at <https://liberapay.com/org-mode>,
> or support my work at <https://liberapay.com/yantar92>
From 9f462d92a959478c5bd8e62b78c905e7b285e1c8 Mon Sep 17 00:00:00 2001
From: ApollonDeParnasse <[email protected]>
Date: Tue, 30 Jun 2026 16:07:54 -0500
Subject: [PATCH] lisp/ox-html: New export option for file links

* lisp/ox-html.el (org-html-base-directory): New export option
for file links.
(org-html--create-file-link-path): Create file link paths for
`org-html-link'.
(org-html-link): Have `org-html--create-file-link-path' create
file link paths.

* testing/lisp/test-ox-html.el (test-ox-html-create-test-link-element):
Helper function for `org-html-link' tests.
(ox-html/test-org-html-base-directory): New tests for
`org-html-link'.
(ox-html/test-link-home-and-use-abs-url/no-base-directory):
New tests for `org-html-link'.
(ox-html/test-org-html-base-directory/with-base-directory-set):
New tests for `org-html-link'.

* lisp/ox.el (org-export-file-relative-name-maybe):
Convert filename into a relative filename if it is in a given
base-directory.

* lisp/ox-publish (org-publish-file-relative-name): Use
`org-export-file-relative-name-maybe` to convert filenames
into relative file names.
---
 doc/org-manual.org           |  32 ++++-
 etc/ORG-NEWS                 |  27 ++++
 lisp/ox-html.el              |  53 +++++---
 lisp/ox-publish.el           |   8 +-
 lisp/ox.el                   |  11 ++
 testing/lisp/test-ox-html.el | 236 +++++++++++++++++++++++++++++++++++
 6 files changed, 344 insertions(+), 23 deletions(-)

diff --git a/doc/org-manual.org b/doc/org-manual.org
index fa0c71c30..6d6ab57d9 100644
--- a/doc/org-manual.org
+++ b/doc/org-manual.org
@@ -13570,6 +13570,13 @@ settings described in [[*Export Settings]].
   #+vindex: org-html-link-up
   The URL for the up link of exported HTML pages (~org-html-link-up~).
 
+- =HTML_BASE_DIRECTORY= ::
+
+  #+cindex: @samp{HTML_BASE_DIRECTORY}, keyword
+  #+vindex: org-html-base-directory
+  The directory of the project that you intend to
+  export (~org-html-base-directory~).
+
 - =HTML_MATHJAX= ::
 
   #+cindex: @samp{HTML_MATHJAX}, keyword
@@ -13826,8 +13833,29 @@ location, then the converted links should work without any further
 manual intervention.  However, to disable this automatic path
 translation, set ~org-html-link-org-files-as-html~ to ~nil~.  When
 disabled, the HTML export backend substitutes the ID-based links in
-the HTML output.  For more about linking files when publishing to
-a directory, see [[*Publishing links]].
+the HTML output.
+
+#+vindex: org-html-base-directory
+~org-html-base-directory~ can be used to set the directory of
+the project that you intend to export. When used in conjunction
+with ~org-html-link-home~ and ~org-html-link-use-abs-url~, file
+links to files in ~org-html-base-directory~ will have the value
+of ~org-html-link-home~ prepended to them.  For example, when
+~org-html-base-directory~ is set to =~/my/project=, ~org-html-link-home~
+is set to =https://orgmode.org= and ~org-html-link-use-abs-url~ is set to ~t~,
+~[[~/my/project/contribute][Contributing to Org]]~ will be exported as
+
+#+begin_example
+"<a href=\"https://orgmode.org/contribute\";>Contributing to Org</a>"
+#+end_example
+
+When ~:base-directory~ and ~org-html-base-directory~ are both set,
+~:base-directory~ takes precedence, i.e., the value of
+~org-html-base-directory~ will be ignored.
+
+~org-html-base-directory~ is the equivalent of the ~:base-directory~
+key of ~org-publish-project-alist~. For more about linking files when
+publishing to a directory, see [[*Publishing links]].
 
 Org files can also have special directives to the HTML export
 backend.  For example, by using =#+ATTR_HTML= lines to specify new
diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index 2e9e8e35b..5976ccf91 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -260,6 +260,33 @@ non-org-mode files, or any other place where org can not use the
 link context to create a description.  See its docstring for more
 information.
 
+*** New export option ~org-html-base-directory~
+
+This export option, nil by default, allows you to set the directory
+of the project that you intend to export.  This variable is the
+equivalent of the ~:base-directory~ key of ~org-publish-project-alist~.
+It is intended for people who use ox-html simply for exporting as
+opposed to publishing.  When used in conjunction with ~org-html-link-home~
+and ~org-html-link-use-abs-url~, file links to files in ~org-html-base-directory~
+will have the value of ~org-html-link-home~ prepended to them.  For example,
+when ~org-html-base-directory~ is set to =~/my/project=, ~org-html-link-home~
+is set to =https://orgmode.org= and ~org-html-link-use-abs-url~ is set to ~t~,
+~[[~/my/project/contribute][Contributing to Org]]~ will be exported as
+
+#+begin_example
+"<a href=\"https://orgmode.org/contribute\";>Contributing to Org</a>"
+#+end_example
+
+When ~:base-directory~ and ~org-html-base-directory~ are both set,
+~:base-directory~ takes precedence, i.e., the value of
+~org-html-base-directory~ will be ignored.
+
+The option can be customized either by
+
+1. setting the global variable ~org-html-base-directory~
+2. setting the file local keyword =HTML_BASE_DIRECTORY=
+
+
 ** New functions and changes in function arguments
 
 # This also includes changes in function behavior from Elisp perspective.
diff --git a/lisp/ox-html.el b/lisp/ox-html.el
index 74fd198e2..75dc6bf7b 100644
--- a/lisp/ox-html.el
+++ b/lisp/ox-html.el
@@ -44,6 +44,7 @@
 ;;; Function Declarations
 
 (declare-function htmlize-region "ext:htmlize" (beg end))
+(declare-function org-export-file-relative-name-maybe "ox-publish" (filename base-directory))
 
 (defvar htmlize-css-name-prefix)
 (defvar htmlize-output-type)
@@ -122,6 +123,7 @@
     (:html-link-use-abs-url nil "html-link-use-abs-url" org-html-link-use-abs-url)
     (:html-link-home "HTML_LINK_HOME" nil org-html-link-home)
     (:html-link-up "HTML_LINK_UP" nil org-html-link-up)
+    (:html-base-directory "HTML_BASE_DIRECTORY" nil org-html-base-directory)
     (:html-mathjax "HTML_MATHJAX" nil "" space)
     (:html-equation-reference-format "HTML_EQUATION_REFERENCE_FORMAT" nil org-html-equation-reference-format t)
     (:html-postamble nil "html-postamble" org-html-postamble)
@@ -1501,6 +1503,17 @@ ignored."
   :group 'org-export-html
   :type 'string)
 
+(defcustom org-html-base-directory nil
+  "Directory of the project that you intend to export.
+This variable is the equivalent of the `:base-directory'
+key of `org-publish-project-alist'.  If `:base-directory'
+is also set, the value of this variable will be ignored."
+  :group 'org-export-html
+  :package-version '(Org . "10.0")
+  :type '(choice (const :tag "No base directory" nil)
+		 (string :tag "Base directory of the project"))
+  :safe #'string-or-null-p)
+
 ;;;; Template :: Scripts
 
 (defcustom org-html-head-include-scripts nil
@@ -3325,6 +3338,28 @@ images, set it to:
 	       info nil 'link)
 	     (= link-count 1))))))
 
+(defun org-html--create-file-link-path (raw-path info)
+  "Convert RAW-PATH into a HTML file link path.
+During publishing, turn absolute file names belonging to
+base directory into relative file names.  Otherwise,
+prepend `file' protocol to absolute file name.  INFO
+should be the export options, as a plist."
+  (let* ((html-base-directory (or (plist-get info :base-directory)
+                                  (plist-get info :html-base-directory)))
+         (maybe-file-relative-name (org-export-file-relative-name-maybe
+                              raw-path html-base-directory))
+         (home (and (plist-get info :html-link-home)
+                    (org-trim (plist-get info :html-link-home)))))
+    ;; Possibly prepend `:html-link-home' to relative file
+    ;; name.
+    (if (and home
+             (plist-get info :html-link-use-abs-url)
+             (not (file-name-absolute-p maybe-file-relative-name)))
+        (file-name-concat
+         (file-name-as-directory home)
+         maybe-file-relative-name)
+      (org-export-file-uri maybe-file-relative-name))))
+
 (defun org-html-link (link desc info)
   "Transcode a LINK object from Org to HTML.
 DESC is the description part of the link, or the empty string.
@@ -3350,22 +3385,10 @@ INFO is a plist holding contextual information.  See
 	 (path
 	  (cond
 	   ((string= "file" type)
-	    ;; During publishing, turn absolute file names belonging
-	    ;; to base directory into relative file names.  Otherwise,
-	    ;; append "file" protocol to absolute file name.
-	    (setq raw-path
-		  (org-export-file-uri
-		   (org-publish-file-relative-name raw-path info)))
-	    ;; Possibly append `:html-link-home' to relative file
-	    ;; name.
-	    (let ((home (and (plist-get info :html-link-home)
-			     (org-trim (plist-get info :html-link-home)))))
-	      (when (and home
-			 (plist-get info :html-link-use-abs-url)
-			 (not (file-name-absolute-p raw-path)))
-		(setq raw-path (concat (file-name-as-directory home) raw-path))))
 	    ;; Maybe turn ".org" into ".html".
-	    (setq raw-path (funcall link-org-files-as-html-maybe raw-path info))
+	    (setq raw-path (funcall link-org-files-as-html-maybe
+                                    (org-html--create-file-link-path raw-path info)
+                                    info))
 	    ;; Add search option, if any.  A search option can be
 	    ;; relative to a custom-id, a headline title, a name or
 	    ;; a target.
diff --git a/lisp/ox-publish.el b/lisp/ox-publish.el
index d51d68f31..27c1a3086 100644
--- a/lisp/ox-publish.el
+++ b/lisp/ox-publish.el
@@ -45,6 +45,7 @@
 (require 'ox)
 
 (declare-function org-at-heading-p "org" (&optional _))
+(declare-function org-export-file-relative-name-maybe "ox-publish" (filename base-directory))
 
 
 ;;; Variables
@@ -1206,12 +1207,7 @@ references with `org-export-get-reference'."
 INFO is the plist containing the current export state.  The
 function does not change relative file names."
   (let ((base (plist-get info :base-directory)))
-    (if (and base
-	     (file-name-absolute-p filename)
-	     (file-in-directory-p filename base))
-	(file-relative-name filename base)
-      filename)))
-
+    (org-export-file-relative-name-maybe filename base)))
 
 
 ;;; Caching functions
diff --git a/lisp/ox.el b/lisp/ox.el
index 90c853d76..bd63423bc 100644
--- a/lisp/ox.el
+++ b/lisp/ox.el
@@ -4742,6 +4742,17 @@ downloaded copy.  Otherwise, return unchanged LINK."
                  (org-element-property :raw-link link))))))
   link)
 
+(defun org-export-file-relative-name-maybe (filename base-directory)
+  "Maybe convert FILENAME to a relative filename.
+FILENAME will only be converted to a relative filename
+if it is in BASE-DIRECTORY.  Otherwise, FILENAME will simply
+be returned."
+  (if (and base-directory
+       (file-name-absolute-p filename)
+	   (file-in-directory-p filename base-directory))
+	(file-relative-name filename base-directory)
+      filename))
+
 ;;;; For References
 ;;
 ;; `org-export-get-reference' associate a unique reference for any
diff --git a/testing/lisp/test-ox-html.el b/testing/lisp/test-ox-html.el
index 717838105..85d038cad 100644
--- a/testing/lisp/test-ox-html.el
+++ b/testing/lisp/test-ox-html.el
@@ -1260,4 +1260,240 @@ entirely."
     (org-export-string-as "" 'html nil
                           '( :html-doctype "xhtml5"
                              :html-klipsify-src t)))))
+
+;;; Rendering Links
+
+(defun test-ox-html-create-test-link-element (test-link-path
+                                              &optional test-desc)
+  "Helper function for `ox-html-link' tests.
+Uses TEST-LINK-PATH and TEST-DESC to create a file link.
+That file link will then be converted into an org-element."
+  (let ((test-desc (org-link-make-string test-link-path test-desc)))
+    (org-test-with-temp-text test-desc
+      (org-element-link-parser))))
+
+(ert-deftest ox-html/test-org-html-base-directory ()
+  "Test `org-html-base-directory'."
+  ;; file is not in html-base-directory
+  (org-test-with-temp-text-in-file ""
+    (let* ((test-desc "Install Emacs")
+           (test-dir (file-name-parent-directory buffer-file-name))
+           (test-home "https://www.example.com";)
+           (test-file-name "/en/install-emacs-on-android")
+           (test-link-element (test-ox-html-create-test-link-element
+                               test-file-name
+                               test-desc))
+           (expected-link-path (format "file://%s" test-file-name))
+           (expected-link (format "<a href=\"%s\">%s</a>"
+                                  expected-link-path
+                                  test-desc))
+           (test-info (list :html-base-directory test-dir
+                            :html-link-use-abs-url t
+                            :html-link-home test-home))
+           (actual-link (org-html-link
+                         test-link-element
+                         test-desc
+                         test-info)))
+      (should (string-equal actual-link expected-link))))
+
+  (org-test-with-temp-text-in-file ""
+    (let* ((test-dir (file-name-parent-directory buffer-file-name))
+           (test-home "https://www.notabug.com";)
+           (test-file-name "/examples/babel.html")
+           (test-link-element (test-ox-html-create-test-link-element
+                                test-file-name))
+           (expected-link-path (format "file://%s" test-file-name))
+           (expected-link (format "<a href=\"%s\">%s</a>"
+                                  expected-link-path
+                                  expected-link-path))
+           (test-info (list :html-base-directory test-dir
+                            :html-link-use-abs-url t
+                            :html-link-home test-home))
+           (actual-link (org-html-link
+                         test-link-element
+                         nil
+                         test-info)))
+      (should (string-equal actual-link expected-link))))
+
+  ;; file is in html-base-directory
+  (org-test-with-temp-text-in-file ""
+    (let* ((test-desc "Contributing to Org")
+           (test-home "https://orgmode.org";)
+           (test-dir (file-name-parent-directory buffer-file-name))
+           (test-file-name (file-name-nondirectory buffer-file-name))
+           (test-link-element (test-ox-html-create-test-link-element
+                               buffer-file-name
+                               test-desc))
+           (expected-link-path
+            (file-name-concat test-home test-file-name))
+           (expected-link (format "<a href=\"%s\">%s</a>"
+                                  expected-link-path
+                                  test-desc))
+           (test-info (list :html-base-directory test-dir
+                            :html-link-use-abs-url t
+                            :html-link-home test-home))
+           (actual-link (org-html-link
+                         test-link-element
+                         test-desc
+                         test-info)))
+      (should (string-equal actual-link expected-link))))
+
+  (org-test-with-temp-text-in-file ""
+    (let* ((test-home "https://mywebsite.com";)
+           (test-dir (file-name-parent-directory buffer-file-name))
+           (test-file-name (file-name-nondirectory buffer-file-name))
+           (test-link-element (test-ox-html-create-test-link-element
+                               buffer-file-name))
+           (expected-link-path
+            (file-name-concat test-home test-file-name))
+           (expected-link (format "<a href=\"%s\">%s</a>"
+                                  expected-link-path
+                                  expected-link-path))
+           (test-info (list :html-base-directory test-dir
+                            :html-link-use-abs-url t
+                            :html-link-home test-home))
+           (actual-link (org-html-link
+                         test-link-element
+                         nil
+                         test-info)))
+      (should (string-equal actual-link expected-link)))))
+
+(ert-deftest ox-html/test-link-home-and-use-abs-url/no-base-directory ()
+  "Test `org-html-link-use-abs-url' with `org-html-link-home'."
+  (ert-with-temp-file test-link-path
+    (let* ((test-desc "Contributing to Org")
+           (test-home "https://orgmode.org";)
+           (test-link-element (test-ox-html-create-test-link-element
+                               test-link-path
+                               test-desc))
+           (expected-link-path (format  "file://%s" test-link-path))
+           (expected-link (format "<a href=\"%s\">%s</a>"
+                                  expected-link-path
+                                  test-desc))
+           (test-info (list :html-link-use-abs-url t
+                            :html-link-home test-home))
+           (actual-link (org-html-link
+                         test-link-element
+                         test-desc
+                         test-info)))
+      (should (string-equal actual-link expected-link))))
+
+  (ert-with-temp-file test-link-path
+    (let* ((test-home "https://mywebsite.com";)
+           (test-link-element (test-ox-html-create-test-link-element
+                               test-link-path))
+           (expected-link-path (format  "file://%s" test-link-path))
+           (expected-link (format "<a href=\"%s\">%s</a>"
+                                  expected-link-path
+                                  expected-link-path))
+           (test-info (list :html-link-use-abs-url t
+                            :html-link-home test-home))
+           (actual-link (org-html-link
+                         test-link-element
+                         nil
+                         test-info)))
+      (should (string-equal actual-link expected-link)))))
+
+(ert-deftest ox-html/test-org-html-base-directory/with-base-directory-set ()
+  "Assert `:base-directory' has precendence over `org-html-base-directory'."
+  ;; file is in `:base-directory'
+  ;; and is not in html-base-directory
+  (org-test-with-temp-text-in-file ""
+    (let* ((test-desc "Contributing to Org")
+           (test-home "https://orgmode.org";)
+           (test-base-directory (file-name-parent-directory
+                                 buffer-file-name))
+           (test-html-base-directory "/my/project/directory")
+           (test-file-name (file-name-nondirectory buffer-file-name))
+           (test-link-element (test-ox-html-create-test-link-element
+                               buffer-file-name
+                               test-desc))
+           (expected-link-path
+            (file-name-concat test-home test-file-name))
+           (expected-link (format "<a href=\"%s\">%s</a>"
+                                  expected-link-path
+                                  test-desc))
+           (test-info (list :base-directory test-base-directory
+                            :html-base-directory test-html-base-directory
+                            :html-link-use-abs-url t
+                            :html-link-home test-home))
+           (actual-link (org-html-link
+                         test-link-element
+                         test-desc
+                         test-info)))
+      (should (string-equal actual-link expected-link))))
+
+  (org-test-with-temp-text-in-file ""
+    (let* ((test-home "https://mywebsite.com";)
+           (test-base-directory (file-name-parent-directory
+                                 buffer-file-name))
+           (test-html-base-directory "~/mywebsite")
+           (test-file-name (file-name-nondirectory buffer-file-name))
+           (test-link-element (test-ox-html-create-test-link-element
+                               buffer-file-name))
+           (expected-link-path
+            (file-name-concat test-home test-file-name))
+           (expected-link (format "<a href=\"%s\">%s</a>"
+                                  expected-link-path
+                                  expected-link-path))
+           (test-info (list :base-directory test-base-directory
+                            :html-base-directory test-html-base-directory
+                            :html-link-use-abs-url t
+                            :html-link-home test-home))
+           (actual-link (org-html-link
+                         test-link-element
+                         nil
+                         test-info)))
+      (should (string-equal actual-link expected-link))))
+
+  ;; file is not in `:base-directory'
+  ;; and is in html-base-directory
+  (org-test-with-temp-text-in-file ""
+    (let* ((test-desc "Contributing to Org")
+           (test-home "https://orgmode.org";)
+           (test-base-directory "/org/base")
+           (test-html-base-directory (file-name-parent-directory
+                                   buffer-file-name))
+           (test-file-name (file-name-nondirectory buffer-file-name))
+           (test-link-element (test-ox-html-create-test-link-element
+                               buffer-file-name
+                               test-desc))
+           (expected-link-path (format "file://%s" buffer-file-name))
+           (expected-link (format "<a href=\"%s\">%s</a>"
+                                  expected-link-path
+                                  test-desc))
+           (test-info (list :base-directory test-base-directory
+                            :html-base-directory test-html-base-directory
+                            :html-link-use-abs-url t
+                            :html-link-home test-home))
+           (actual-link (org-html-link
+                         test-link-element
+                         test-desc
+                         test-info)))
+      (should (string-equal actual-link expected-link))))
+
+  (org-test-with-temp-text-in-file ""
+    (let* ((test-home "https://mywebsite.com";)
+           (test-base-directory "~/website/publish-dir")
+           (test-html-base-directory (file-name-parent-directory
+                                   buffer-file-name))
+           (test-file-name (file-name-nondirectory buffer-file-name))
+           (test-link-element (test-ox-html-create-test-link-element
+                               buffer-file-name))
+           (expected-link-path (format "file://%s" buffer-file-name))
+           (expected-link (format "<a href=\"%s\">%s</a>"
+                                  expected-link-path
+                                  expected-link-path))
+           (test-info (list :base-directory test-base-directory
+                            :html-base-directory test-html-base-directory
+                            :html-link-use-abs-url t
+                            :html-link-home test-home))
+           (actual-link (org-html-link
+                         test-link-element
+                         nil
+                         test-info)))
+      (should (string-equal actual-link expected-link)))))
+
+
+
 ;;; test-ox-html.el ends here
-- 
2.54.0

Reply via email to