Skip to content

Commit b720792

Browse files
authored
Unify link handling between build! and serve! (#529)
This unifies the link handling between `build!` and `serve!` by no longer using extensions in either mode (was `.clj|md` in `serve!` and `.html` in `build!`). To support this in the unbundled static build, we're now writing directories with `index.html` for each notebook. This makes links in this build no longer accessible without a http server. If you're looking for a self-contained html that works without a webserver, set the `:bundle` option.
1 parent d801870 commit b720792

File tree

11 files changed

+213
-107
lines changed

11 files changed

+213
-107
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ Changes can be:
3232
* `nextjournal.clerk.viewer/html` instead.
3333

3434
Also rename `nextjournal.clerk.render/html-render` to `nextjournal.clerk.render/render-html` and make `nextjournal.clerk.viewer/html` use it when called from a reactive context.
35+
36+
* 🚨 Unify the link handling between `build!` and `serve!`
37+
38+
By no longer using extensions in either mode (was `.clj|md` in `serve!` and `.html` in `build!`).
39+
40+
To support this in the unbundled static build, we're now writing directories with `index.html` for each notebook. This makes links in this build no longer accessible without a http server. If you're looking for a self-contained html that works without a webserver, set the `:bundle` option.
3541

3642
* 📖 Improve Table of Contents design and fixing re-rendering issues. Also added suport for chapter expansion.
3743

index.clj

+3-2
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@
88
(clerk/html
99
[:div.viewer-markdown
1010
[:ul
11-
[:li [:a.underline {:href (clerk/doc-url "notebooks/rule_30.clj")} "Rule 30"]]
12-
[:li [:a.underline {:href (clerk/doc-url "notebooks/markdown.md")} "Markdown"]]]])
11+
[:li [:a.underline {:href (clerk/doc-url "notebooks/rule_30")} "Rule 30"]]
12+
[:li [:a.underline {:href (clerk/doc-url "notebooks/links")} "Link Design"]]
13+
[:li [:a.underline {:href (clerk/doc-url "notebooks/markdown")} "Markdown"]]]])

notebooks/document_linking.clj

+7-7
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
;; The helper `clerk/doc-url` allows to reference notebooks by path. We currently support relative paths with respect to the directory which started the Clerk application. An optional trailing hash fragment can appended to the path in order for the page to be scrolled up to the indicated identifier.
88
(clerk/html
99
[:ol
10-
[:li [:a {:href (clerk/doc-url "notebooks/viewers/html.clj")} "HTML"]]
11-
[:li [:a {:href (clerk/doc-url "notebooks/viewers/image.clj")} "Images"]]
10+
[:li [:a {:href (clerk/doc-url "notebooks/viewers/html")} "HTML"]]
11+
[:li [:a {:href (clerk/doc-url "notebooks/viewers/image")} "Images"]]
1212
[:li [:a {:href (clerk/doc-url "notebooks/markdown.md" "appendix")} "Markdown / Appendix"]]
13-
[:li [:a {:href (clerk/doc-url "notebooks/how_clerk_works.clj" "step-3:-analyzer")} "Clerk Analyzer"]]
14-
[:li [:a {:href (clerk/doc-url "book.clj")} "The 📕Book"]]
13+
[:li [:a {:href (clerk/doc-url "notebooks/how_clerk_works" "step-3:-analyzer")} "Clerk Analyzer"]]
14+
[:li [:a {:href (clerk/doc-url "book")} "The 📕Book"]]
1515
[:li [:a {:href (clerk/doc-url "")} "Homepage"]]])
1616

1717

@@ -20,6 +20,6 @@
2020
(clerk/with-viewer
2121
'(fn [_ _]
2222
[:ol
23-
[:li [:a {:href (nextjournal.clerk.viewer/doc-url "notebooks/viewers/html.clj")} "HTML"]]
24-
[:li [:a {:href (nextjournal.clerk.viewer/doc-url "notebooks/markdown.md")} "Markdown"]]
25-
[:li [:a {:href (nextjournal.clerk.viewer/doc-url "notebooks/viewer_api.clj")} "Viewer API / Tables"]]]) nil)
23+
[:li [:a {:href (nextjournal.clerk.viewer/doc-url "notebooks/viewers/html")} "HTML"]]
24+
[:li [:a {:href (nextjournal.clerk.viewer/doc-url "notebooks/markdown")} "Markdown"]]
25+
[:li [:a {:href (nextjournal.clerk.viewer/doc-url "notebooks/viewer_api")} "Viewer API / Tables"]]]) nil)

notebooks/links.md

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Links
2+
```clojure
3+
(ns links
4+
{:nextjournal.clerk/toc true}
5+
(:require [nextjournal.clerk :as clerk]))
6+
```
7+
## Design
8+
9+
We have three different modes to consider for links:
10+
11+
1. Interactive mode `serve!`
12+
2. Static build unbundled `(build! {:bundle false})`
13+
3. Static build bundled `(build! {:bundle true})`
14+
15+
The behaviour when triggering links we want is:
16+
17+
1. Interactive mode: trigger a js event `(clerk-eval 'nextjournal.clerk.webserver/navigate! ,,,)`, the doc will in turn be updated via the websocket
18+
2. Static build unbundled: not intercept the link, let the browser perform its normal navigation
19+
3. Static build bundled: trigger a js event to update the doc, update the browser's hash so the doc state is persisted on reload
20+
21+
We can allow folks to write normal (relative) links. The limitations here being that things like open in new tab would not work and we can't support a routing function. Both these limitations means we probably want to continue encouraging the use of a helper like `clerk/doc-url` going forward.
22+
23+
We currently don't support navigating to headings / table of contents sections in the bundled build. This could be supported however by introducing a way to encode that in the hash e.g. with `#page:section`.
24+
25+
26+
## Examples
27+
28+
29+
### JVM-Side
30+
31+
The helper `clerk/doc-url` allows to reference notebooks by path. We currently support relative paths with respect to the directory which started the Clerk application. An optional trailing hash fragment can appended to the path in order for the page to be scrolled up to the indicated identifier.
32+
33+
34+
```clojure
35+
(clerk/html
36+
[:ol
37+
[:li [:a {:href (clerk/doc-url 'nextjournal.clerk.home)} "Home"]]
38+
[:li [:a {:href (clerk/doc-url "notebooks/viewers/html")} "HTML"]]
39+
[:li [:a {:href (clerk/doc-url "notebooks/viewers/image")} "Images"]]
40+
[:li [:a {:href (clerk/doc-url "notebooks/markdown.md" "appendix")} "Markdown / Appendix"]]
41+
[:li [:a {:href (clerk/doc-url "notebooks/how_clerk_works" "step-3:-analyzer")} "Clerk Analyzer"]]
42+
[:li [:a {:href (clerk/doc-url "book")} "The 📕Book"]]
43+
[:li [:a {:href (clerk/doc-url "")} "Homepage"]]])
44+
```
45+
46+
### Render
47+
48+
The same functionality is available in the SCI context when building render functions.
49+
50+
```clojure
51+
(clerk/with-viewer
52+
'(fn [_ _]
53+
[:ol
54+
[:li [:a {:href (nextjournal.clerk.viewer/doc-url "notebooks/viewers/html")} "HTML"]]
55+
[:li [:a {:href (nextjournal.clerk.viewer/doc-url "notebooks/markdown")} "Markdown"]]
56+
[:li [:a {:href (nextjournal.clerk.viewer/doc-url "notebooks/viewer_api")} "Viewer API / Tables"]]]) nil)
57+
58+
```
59+
60+
61+
### Inside Markdown
62+
63+
Links should work inside markdown as well.
64+
65+
* [HTML](../notebooks/viewers/html) (relative link)
66+
* [HTML](clerk/doc-url,"notebooks/viewers/html") (doc url, currently not functional)
67+

src/nextjournal/clerk/builder.clj

+35-32
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,9 @@
205205
(expand-paths {:paths-fn `my-paths}))
206206
#_(expand-paths {:paths ["notebooks/viewers**"]})
207207

208+
(def builtin-index
209+
(io/resource "nextjournal/clerk/index.clj"))
210+
208211
(defn process-build-opts [{:as opts :keys [paths index expand-paths?]}]
209212
(merge {:out-path default-out-path
210213
:bundle? false
@@ -223,29 +226,19 @@
223226
(assoc :index (first expanded-paths))
224227
(and (not index) (< 1 (count expanded-paths)) (every? (complement #{"index.clj"}) expanded-paths))
225228
(as-> opts
226-
(let [index (io/resource "nextjournal/clerk/index.clj")]
227-
(-> opts (assoc :index index) (update :expanded-paths conj index)))))))))
229+
(-> opts (assoc :index builtin-index) (update :expanded-paths conj builtin-index))))))))
228230

229231
#_(process-build-opts {:index 'book.clj :expand-paths? true})
230232
#_(process-build-opts {:paths ["notebooks/rule_30.clj"] :expand-paths? true})
231233
#_(process-build-opts {:paths ["notebooks/rule_30.clj"
232234
"notebooks/markdown.md"] :expand-paths? true})
233235

234-
(defn build-path->url [{:as opts :keys [bundle?]} docs]
235-
(into {}
236-
(map (comp (juxt identity #(cond-> (->> % (viewer/map-index opts) strip-index) (not bundle?) ->html-extension))
237-
str :file))
238-
docs))
239-
#_(build-path->url {:bundle? false} [{:file "notebooks/foo.clj"} {:file "index.clj"}])
240-
#_(build-path->url {:bundle? true} [{:file "notebooks/foo.clj"} {:file "index.clj"}])
241-
242236
(defn build-static-app-opts [{:as opts :keys [bundle? out-path browse? index]} docs]
243-
(let [path->doc (into {} (map (juxt (comp str :file) :viewer)) docs)]
237+
(let [path->doc (into {} (map (juxt (comp str fs/strip-ext strip-index (partial viewer/map-index opts) :file) :viewer)) docs)]
244238
(assoc opts
245239
:bundle? bundle?
246240
:path->doc path->doc
247-
:paths (vec (keys path->doc))
248-
:path->url (build-path->url opts docs))))
241+
:paths (vec (keys path->doc)))))
249242

250243
(defn ssr!
251244
"Shells out to node to generate server-side-rendered html."
@@ -268,28 +261,30 @@
268261

269262
(defn cleanup [build-opts]
270263
(select-keys build-opts
271-
[:bundle? :path->doc :path->url :current-path :resource->url :exclude-js? :index :html]))
264+
[:bundle? :path->doc :current-path :resource->url :exclude-js? :index :html]))
272265

273266
(defn write-static-app!
274267
[opts docs]
275-
(let [{:as opts :keys [bundle? out-path browse? ssr?]} (process-build-opts opts)
268+
(let [{:keys [bundle? out-path browse? ssr?]} opts
276269
index-html (str out-path fs/file-separator "index.html")
277-
{:as static-app-opts :keys [path->url path->doc]} (build-static-app-opts (viewer/update-if opts :index str) docs)]
278-
(when-not (contains? (-> path->url vals set) "")
279-
(throw (ex-info "Index must have been processed at this point" {:opts opts :docs docs})))
270+
{:as static-app-opts :keys [path->doc]} (build-static-app-opts opts docs)]
271+
(when-not (contains? (set (keys path->doc)) "")
272+
(throw (ex-info "Index must have been processed at this point" {:static-app-opts static-app-opts})))
280273
(when-not (fs/exists? (fs/parent index-html))
281274
(fs/create-dirs (fs/parent index-html)))
282275
(if bundle?
283276
(spit index-html (view/->html (cleanup static-app-opts)))
284277
(doseq [[path doc] path->doc]
285-
(let [out-html (str out-path fs/file-separator (->> path (viewer/map-index opts) ->html-extension))]
278+
(let [out-html (fs/file out-path path "index.html")]
286279
(fs/create-dirs (fs/parent out-html))
287280
(spit out-html (view/->html (-> static-app-opts
288281
(assoc :path->doc (hash-map path doc) :current-path path)
289282
(cond-> ssr? ssr!)
290283
cleanup))))))
291284
(when browse?
292-
(browse/browse-url (-> index-html fs/absolutize .toString path-to-url-canonicalize)))
285+
(browse/browse-url (if-let [{:keys [port]} (and (= out-path "public/build") @webserver/!server)]
286+
(str "http://localhost:" port "/build/")
287+
(-> index-html fs/absolutize .toString path-to-url-canonicalize))))
293288
{:docs docs
294289
:index-html index-html
295290
:build-href (if (and @webserver/!server (= out-path default-out-path)) "/build/" index-html)}))
@@ -331,13 +326,13 @@
331326
(update opts :resource->url assoc "/css/viewer.css" url))))
332327

333328
(defn doc-url
334-
([opts doc file path] (doc-url opts doc file path nil))
335-
([{:as opts :keys [bundle?]} docs file path fragment]
336-
(let [url (get (build-path->url (viewer/update-if opts :index str) docs) path)]
337-
(if bundle?
338-
(str "#/" url)
339-
(str (viewer/relative-root-prefix-from (viewer/map-index opts file))
340-
url (when fragment (str "#" fragment)))))))
329+
([opts file path] (doc-url opts file path nil))
330+
([opts file path fragment]
331+
(if (:bundle? opts)
332+
(cond-> (str "#/" path)
333+
fragment (str ":" fragment))
334+
(str (viewer/relative-root-prefix-from (viewer/map-index opts file)) path
335+
(when fragment (str "#" fragment))))))
341336

342337
(defn read-opts-from-deps-edn! []
343338
(if (fs/exists? "deps.edn")
@@ -395,10 +390,14 @@
395390
(try
396391
(binding [*ns* *ns*
397392
*build-opts* opts
398-
viewer/doc-url (partial doc-url opts state file)]
393+
viewer/doc-url (partial doc-url opts file)]
399394
(let [doc (eval/eval-analyzed-doc doc)]
400-
(assoc doc :viewer (view/doc->viewer (assoc opts :static-build? true
401-
:nav-path (str file)) doc))))
395+
(assoc doc :viewer (view/doc->viewer (assoc opts
396+
:static-build? true
397+
:nav-path (if (instance? java.net.URL file)
398+
(str "'" (:ns doc))
399+
(str file)))
400+
doc))))
402401
(catch Exception e
403402
{:error e})))]
404403
(report-fn (merge {:stage :built :duration duration :idx idx}
@@ -422,8 +421,11 @@
422421

423422
(comment
424423
(build-static-app! {:paths clerk-docs :bundle? true})
425-
(build-static-app! {:paths ["notebooks/index.clj" "notebooks/rule_30.clj" "notebooks/viewer_api.md"] :index "notebooks/index.clj"})
426-
(build-static-app! {:paths ["index.clj" "notebooks/rule_30.clj" "notebooks/markdown.md"] :bundle? false :browse? false})
424+
(build-static-app! {:paths ["notebooks/editor.clj"] :browse? true})
425+
(build-static-app! {:paths ["CHANGELOG.md" "notebooks/editor.clj"] :browse? true})
426+
(build-static-app! {:paths ["index.clj" "notebooks/links.md" "notebooks/rule_30.clj" "notebooks/markdown.md"] :bundle? true :browse? true})
427+
(build-static-app! {:paths ["notebooks/links.md" "notebooks/rule_30.clj" "notebooks/markdown.md"] :bundle? true :browse? true})
428+
(build-static-app! {:paths ["index.clj" "notebooks/rule_30.clj" "notebooks/markdown.md"] :bundle? false :browse? true})
427429
(build-static-app! {:paths ["notebooks/viewers/**"]})
428430
(build-static-app! {:index "notebooks/rule_30.clj" :git/sha "bd85a3de12d34a0622eb5b94d82c9e73b95412d1" :git/url "https://github.com/nextjournal/clerk"})
429431
(reset! config/!resource->url @config/!asset-map)
@@ -455,3 +457,4 @@
455457
:bundle? true
456458
:git/sha "d60f5417"
457459
:git/url "https://github.com/nextjournal/clerk"}))
460+

src/nextjournal/clerk/builder_ui.clj

+4-4
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,18 @@
2222

2323
(defn checkmark-svg [& [{:keys [size] :or {size 18}}]]
2424
[:div.flex.justify-center {:class "w-[24px] h-[24px]"}
25-
[:svg {:xmlns "http://www.w3.org/2000/svg", :fill "none", :viewbox "0 0 24 24", :stroke-width "1.5", :stroke "currentColor", :class "w-6 h-6"}
25+
[:svg {:xmlns "http://www.w3.org/2000/svg", :fill "none", :viewBox "0 0 24 24", :stroke-width "1.5", :stroke "currentColor", :class "w-6 h-6"}
2626
[:path {:stroke-linecap "round", :stroke-linejoin "round", :d "M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"}]]])
2727

2828
(defn error-svg [& [{:keys [size] :or {size 18}}]]
2929
[:div.flex.justify-center {:class "w-[24px] h-[24px]"}
3030
[:svg.text-red-400
31-
{:xmlns "http://www.w3.org/2000/svg", :viewbox "0 0 20 20", :fill "currentColor"
31+
{:xmlns "http://www.w3.org/2000/svg", :viewBox "0 0 20 20", :fill "currentColor"
3232
:style {:width size :height size}}
3333
[:path {:fill-rule "evenodd", :d "M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z", :clip-rule "evenodd"}]]])
3434

3535
(def publish-icon-svg
36-
[:svg {:width "18", :height "18", :viewbox "0 0 18 18", :fill "none", :xmlns "http://www.w3.org/2000/svg"}
36+
[:svg {:width "18", :height "18", :viewBox "0 0 18 18", :fill "none", :xmlns "http://www.w3.org/2000/svg"}
3737
[:path {:d "M9 17C12.7267 17 15.8583 14.4517 16.7473 11.0026M9 17C5.27327 17 2.14171 14.4517 1.25271 11.0026M9 17C11.2091 17 13 13.4183 13 9C13 4.58172 11.2091 1 9 1M9 17C6.79086 17 5 13.4183 5 9C5 4.58172 6.79086 1 9 1M9 1C11.9913 1 14.5991 2.64172 15.9716 5.07329M9 1C6.00872 1 3.40088 2.64172 2.02838 5.07329M15.9716 5.07329C14.102 6.68924 11.6651 7.66667 9 7.66667C6.33486 7.66667 3.89802 6.68924 2.02838 5.07329M15.9716 5.07329C16.6264 6.23327 17 7.573 17 9C17 9.69154 16.9123 10.3626 16.7473 11.0026M16.7473 11.0026C14.4519 12.2753 11.8106 13 9 13C6.18943 13 3.54811 12.2753 1.25271 11.0026M1.25271 11.0026C1.08775 10.3626 1 9.69154 1 9C1 7.573 1.37362 6.23327 2.02838 5.07329", :stroke "currentColor", :stroke-linecap "round", :stroke-linejoin "round"}]])
3838

3939
^{:nextjournal.clerk/visibility {:result :show}}
@@ -304,7 +304,7 @@
304304
(checkmark-svg)
305305
[:div.text-lg.ml-2.mb-0.font-medium "Your notebooks have been built."]]
306306
[:a.font-medium.rounded-full.text-sm.px-3.py-1.bg-greenish-20.flex.items-center.border-2.border-greenish.animate-border-pulse.hover:border-white.hover:animate-none
307-
{:href link}
307+
{:href link :data-ignore-anchor-click true}
308308
[:div publish-icon-svg]
309309
[:span.ml-2 "Open"]]]
310310
[:div.flex.items-center.px-4.lg:px-0

src/nextjournal/clerk/index.clj

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
[:li.border-t.first:border-t-0.dark:border-gray-800.odd:bg-slate-50.dark:odd:bg-white
1616
{:class "dark:odd:bg-opacity-[0.03]"}
1717
[:a.pl-4.pr-4.py-2.flex.w-full.items-center.justify-between.hover:bg-indigo-50.dark:hover:bg-gray-700
18-
{:href (clerk/doc-url path)}
18+
{:href (clerk/doc-url (fs/strip-ext path))}
1919
[:span.text-sm.md:text-md.monospace.flex-auto.block.truncate path]
2020
[:svg.h-4.w-4.flex-shrink-0 {:xmlns "http://www.w3.org/2000/svg" :fill "none" :viewBox "0 0 24 24" :stroke "currentColor"}
2121
[:path {:stroke-linecap "round" :stroke-linejoin "round" :stroke-width "2" :d "M9 5l7 7-7 7"}]]]])))})

0 commit comments

Comments
 (0)