diff --git a/docs/difference-from-coleslaw.lisp b/docs/difference-from-coleslaw.lisp index 68788df..888c098 100644 --- a/docs/difference-from-coleslaw.lisp +++ b/docs/difference-from-coleslaw.lisp @@ -3,18 +3,32 @@ " Staticl config is constructed from lisp function calls and you can benefit from IDE's code completion. -Variable `config` was renamed to `site`. - post -> content - pubdate -> site.pubdate. - site.sitenav -> site.navigation.items also for items inside navigation now have a \"title\" slot instead of \"name\" and also navigation menu can contain submenus, but this requires a special support from the theme. If an item has slot \"item\", then it is a submenu. +The first important variable `config` was renamed to `site`. Secondly, variables `post` and `index` were renamed to `content`. That is it - content of any page, be it a post or a generic page is available as `content` variable inside the template. - index -> content - index.content -> content.items - prev -> content.prev - next -> content.next - tags -> content.tags - obj.date -> obj.created_at +## Changes in navigation +The `site.sitenav` list was renamed to `site.navigation.items`. Also items inside the navigation now have a `title` slot instead of `name` and also navigation menu can contain submenus, but this requires a special support from the theme. If an item has a slot `item`, then it is a submenu. Themes ported from the Coleslaw do not support this submenues. + +## Index pages + +For index pages a list of items was also moved and now instead of `index.content` a `content.items` should be used. + +### Index objects + +For objects in `content.items` attribute `obj.text` was renamed to `obj.excerpt`. It is a HTML, so `noAutoescape` filter should be applied (as you did in Coleslaw themes too). + +## Other field renames + +- `pubdate -> site.pubdate` +- `obj.date -> obj.created_at` +- `post.date -> content.created_at` +- `post.text -> content.html` +- `tags -> content.tags` +- `prev -> content.prev` +- `next -> content.next` + + +## Working with dates For templates base on Closure Template, StatiCL defines these filters: @@ -23,15 +37,20 @@ For templates base on Closure Template, StatiCL defines these filters: To define additional filters, inherit your template class from CLOSURE-TEMPLATE and define a method for REGISTER-USER-FILTERS generic-function. -Instead of makin 1.html and symlinking to it from index.html, StatiCL just generates first page as index.html and other pages as 2.html, 3.html, etc. - - -URLs +## URLs Templates in Coleslaw used {$site.domain}/ as a prefix to each URL. With StatiCL all URLs are formatted in advance before variables are passed to the template and you don't have to concatenate string to get a proper URL in a template. -Additional formats +Also, note, that instead of making 1.html and symlinking to it from index.html for post indices, StatiCL just generates first page as index.html and other pages as 2.html, 3.html, etc.. + + +## Pages layout + +Coleslaw uses a subfolder `pages/` to keep content of all site pages and put them to the root of the site. Staticl does not implement this logic - it generates an output page with the same path as an original file. For example, if previousl with Coleslaw you put `/pages/about.post` to get `/about.html` page, with Staticl you write `/about.post` source file to generate `/about.html` or `/about/index.html` (depending on clean urls setting). + + +## Additional formats * (ql:quickload :staticl/format/spinneret) diff --git a/example/.staticlrc b/example/.staticlrc index a778559..29be7d2 100644 --- a/example/.staticlrc +++ b/example/.staticlrc @@ -9,7 +9,7 @@ :url "https://example.com/" :navigation (menu (item "Blog (EN)" "/blog/") (item "Blog (RU)" "/ru/blog/") - (item "About" "/about.html")) + (item "About" "/about/")) :pipeline (list (load-content) (filter (:path "ru/") (prev-next-links) diff --git a/example/ru/index.post b/example/ru/index.page similarity index 100% rename from example/ru/index.post rename to example/ru/index.page diff --git a/qlfile.lock b/qlfile.lock index 0aa8290..890c2fb 100644 --- a/qlfile.lock +++ b/qlfile.lock @@ -1,11 +1,11 @@ ("quicklisp" . (:class qlot/source/dist:source-dist - :initargs (:distribution "http://beta.quicklisp.org/dist/quicklisp.txt" :%version :latest) + :initargs (:distribution "https://beta.quicklisp.org/dist/quicklisp.txt" :%version :latest) :version "2023-10-21")) ("ultralisp" . (:class qlot/source/dist:source-dist - :initargs (:distribution "http://dist.ultralisp.org/" :%version :latest) - :version "20240330015000")) + :initargs (:distribution "https://dist.ultralisp.org/" :%version :latest) + :version "20240428124000")) ("slynk" . (:class qlot/source/github:source-github :initargs (:repos "svetlyak40wt/sly" :ref nil :branch "patches" :tag nil) diff --git a/src/clean-urls.lisp b/src/clean-urls.lisp new file mode 100644 index 0000000..015fbce --- /dev/null +++ b/src/clean-urls.lisp @@ -0,0 +1,73 @@ +(uiop:define-package #:staticl/clean-urls + (:use #:cl) + (:import-from #:staticl/site + #:clean-urls-p + #:site) + (:import-from #:str + #:ends-with-p) + (:import-from #:serapeum + #:->) + (:export #:transform-url + #:transform-filename)) +(in-package #:staticl/clean-urls) + + +(-> clean-url (string) + (values string &optional)) + +(defun clean-url (url) + (cond + ((ends-with-p "/index.html" url) + (subseq url 0 (1+ (- (length url) + (length "/index.html"))))) + ((ends-with-p ".html" url) + (concatenate 'string + (subseq url 0 (- (length url) + (length ".html"))) + "/")) + (t + url))) + + +(-> clean-pathname (pathname) + (values pathname &optional)) + +(defun clean-pathname (filename) + (cond + ((and (string-equal (pathname-type filename) + "html") + (not (string-equal (pathname-name filename) + "index"))) + (merge-pathnames + (make-pathname :directory (list :relative (pathname-name filename)) + :name "index" + :type "html") + filename)) + (t + filename))) + + +(defgeneric transform-url (site url) + (:documentation "Converts the URL to the form that should be used on the site. + + If the site has the clean-urls setting enabled, then the URL like /some/page.html will be converted + to /some/page/. If clean-urls is not enabled, the URL will remain unchanged.") + (:method ((site site) (url string)) + (cond + ((clean-urls-p site) + (clean-url url)) + (t + url)))) + + +(defgeneric transform-filename (site filename) + (:documentation "Converts the pathname to the form that should be used to write content to the disk. + + If the site has the clean-urls setting enabled, then the filename like some/page.html will be converted + to some/page/index.html. If clean-urls is not enabled, the pathname will remain unchanged.") + (:method ((site site) (filename pathname)) + (cond + ((and (clean-urls-p site)) + (clean-pathname filename)) + (t + filename)))) diff --git a/src/content.lisp b/src/content.lisp index f61fe8e..a9035ed 100644 --- a/src/content.lisp +++ b/src/content.lisp @@ -1,7 +1,5 @@ (uiop:define-package #:staticl/content (:use #:cl) - (:import-from #:staticl/theme - #:template-vars) (:import-from #:serapeum #:-> #:dict) @@ -13,6 +11,7 @@ #:site-content-root #:site) (:import-from #:alexandria + #:curry #:with-output-to-file #:length=) (:import-from #:staticl/utils @@ -45,6 +44,8 @@ (:import-from #:staticl/content/html-content #:content-html-excerpt #:content-html) + (:import-from #:staticl/clean-urls + #:transform-filename) (:export #:supported-content-types #:content-type #:content @@ -249,10 +250,14 @@ (merge-pathnames (merge-pathnames (make-pathname :type "html") relative-path) - stage-dir)))) + stage-dir))) + + (:method :around ((site site) (content content) (stage-dir pathname)) + (transform-filename site + (call-next-method)))) -(defmethod object-url ((content content-from-file) &key &allow-other-keys) +(defmethod object-url ((site site) (content content-from-file) &key &allow-other-keys) (or (slot-value content 'url) (let* ((root (current-root)) (relative-path (enough-namestring (content-file content) @@ -267,8 +272,8 @@ (:method ((site site) (content content) (stream stream)) (let* ((theme (site-theme site)) - (content-vars (template-vars content)) - (site-vars (template-vars site)) + (content-vars (template-vars site content)) + (site-vars (template-vars site site)) (vars (dict "site" site-vars "content" content-vars)) (template-name (content-template content))) @@ -280,9 +285,9 @@ (:documentation "Returns an additional list content objects such as RSS feeds or sitemaps.")) -(defmethod template-vars :around ((content content) &key (hash (dict))) +(defmethod template-vars :around ((site site) (content content) &key (hash (dict))) (loop with result = (if (next-method-p) - (call-next-method content :hash hash) + (call-next-method site content :hash hash) (values hash)) for key being the hash-key of (content-metadata content) using (hash-value value) @@ -295,7 +300,7 @@ ;; Here we need transform CLOS objects to hash-tables ;; to make their fields accessable in the template (standard-object - (template-vars value)) + (template-vars site value)) ;; Other types are passed as is: (t value))) @@ -319,7 +324,7 @@ (content-format content)))) -(defmethod template-vars ((content content-from-file) &key (hash (dict))) +(defmethod template-vars ((site site) (content content-from-file) &key (hash (dict))) (setf (gethash "title" hash) (content-title content) (gethash "html" hash) @@ -343,13 +348,13 @@ ) (if (next-method-p) - (call-next-method content :hash hash) + (call-next-method site content :hash hash) (values hash))) -(defmethod template-vars ((content content-with-tags-mixin) &key (hash (dict))) +(defmethod template-vars ((site site) (content content-with-tags-mixin) &key (hash (dict))) (setf (gethash "tags" hash) - (mapcar #'template-vars + (mapcar (curry #'template-vars site) (content-tags content))) (if (next-method-p) diff --git a/src/core.lisp b/src/core.lisp index f44be4c..f3f98a2 100644 --- a/src/core.lisp +++ b/src/core.lisp @@ -19,7 +19,6 @@ (:import-from #:staticl/current-root #:with-current-root) (:import-from #:staticl/url - #:object-url #:with-base-url) (:nicknames #:staticl/core) (:export #:generate diff --git a/src/feeds/base.lisp b/src/feeds/base.lisp index ae31fa4..efaab6a 100644 --- a/src/feeds/base.lisp +++ b/src/feeds/base.lisp @@ -74,16 +74,16 @@ (defmethod staticl/content:write-content-to-stream ((site site) (feed-file feed-file) stream) (loop for item in (content-items feed-file) for feed-entry = (make-instance 'org.shirakumo.feeder:entry - :id (staticl/url:object-url item :full t) - :link (staticl/url:object-url item :full t) + :id (staticl/url:object-url site item :full t) + :link (staticl/url:object-url site item :full t) :title (staticl/content:content-title item) :summary (staticl/content/html-content:content-html-excerpt item) :content (staticl/content::content-html item)) collect feed-entry into entries finally (let* ((feed (make-instance 'org.shirakumo.feeder:feed - :id (staticl/url:object-url site :full t) - :link (staticl/url:object-url site :full t) + :id (staticl/url:object-url site site :full t) + :link (staticl/url:object-url site site :full t) :title (staticl/site:site-title site) :summary (staticl/site:site-description site) :content entries)) @@ -92,7 +92,7 @@ (plump:serialize plump-node stream)))) -(defmethod object-url ((feed-file feed-file) &key &allow-other-keys) +(defmethod object-url ((site site) (feed-file feed-file) &key &allow-other-keys) (let* ((root (current-root)) (relative-path (enough-namestring (target-path feed-file) root))) diff --git a/src/index/base.lisp b/src/index/base.lisp index 6f205e5..5abdb4b 100644 --- a/src/index/base.lisp +++ b/src/index/base.lisp @@ -90,10 +90,10 @@ -(defmethod template-vars ((content index-page) &key (hash (dict))) +(defmethod template-vars ((site site) (content index-page) &key (hash (dict))) (flet ((item-vars (item) (dict "url" - (staticl/url:object-url item) + (staticl/url:object-url site item) "title" (staticl/content:content-title item) "created-at" @@ -110,19 +110,19 @@ (when (prev-page content) (dict "url" - (staticl/url:object-url (prev-page content)))) + (staticl/url:object-url site (prev-page content)))) (gethash "next" hash) (when (next-page content) (dict "url" - (staticl/url:object-url (next-page content)))))) + (staticl/url:object-url site (next-page content)))))) (if (next-method-p) (call-next-method content :hash hash) (values hash))) -(defmethod object-url ((index index-page) &key &allow-other-keys) +(defmethod object-url ((site site) (index index-page) &key &allow-other-keys) (let* ((root (current-root)) (relative-path (enough-namestring (page-target-path index) root))) diff --git a/src/index/paginated.lisp b/src/index/paginated.lisp index 76942d7..659ed71 100644 --- a/src/index/paginated.lisp +++ b/src/index/paginated.lisp @@ -84,7 +84,10 @@ (defmethod staticl/pipeline:process-items ((site site) (index paginated-index) content-items) (loop with only-posts = (remove-if-not #'postp content-items) - for batch in (serapeum:batches only-posts (page-size index)) + with sorted-posts = (sort only-posts + #'local-time:timestamp> + :key #'staticl/content:content-created-at ) + for batch in (serapeum:batches sorted-posts (page-size index)) for page-number upfrom 1 collect (make-instance 'index-page :title (funcall (page-title-fn index) diff --git a/src/links/link.lisp b/src/links/link.lisp index 146ef44..1969a21 100644 --- a/src/links/link.lisp +++ b/src/links/link.lisp @@ -9,6 +9,8 @@ #:->) (:import-from #:staticl/url #:object-url) + (:import-from #:staticl/site + #:site) (:export #:link)) (in-package #:staticl/links/link) @@ -32,9 +34,9 @@ :content content)) -(defmethod staticl/theme:template-vars ((link link) &key (hash (dict))) +(defmethod staticl/theme:template-vars ((site site) (link link) &key (hash (dict))) (dict* hash "url" - (object-url (link-content link)) + (object-url site (link-content link)) "title" (content-title (link-content link)))) diff --git a/src/plugins/sitemap.lisp b/src/plugins/sitemap.lisp index 206c1a5..738072c 100644 --- a/src/plugins/sitemap.lisp +++ b/src/plugins/sitemap.lisp @@ -48,7 +48,7 @@ (defmethod write-content-to-stream ((site site) (sitemap sitemap-file) (stream stream)) (render-sitemap (loop for item in (sitemap-content sitemap) - collect (make-url (object-url item :full t) + collect (make-url (object-url site item :full t) :changefreq :weekly :priority 0.5)) :stream stream)) diff --git a/src/site-url.lisp b/src/site-url.lisp new file mode 100644 index 0000000..1f20736 --- /dev/null +++ b/src/site-url.lisp @@ -0,0 +1,12 @@ +(uiop:define-package #:staticl/site-url + (:use #:cl) + (:import-from #:staticl/site + #:site-url + #:site) + (:import-from #:staticl/url + #:object-url)) +(in-package #:staticl/site-url) + + +(defmethod object-url ((site site) (obj site) &key &allow-other-keys) + (site-url obj)) diff --git a/src/site.lisp b/src/site.lisp index 584da76..0dc25ed 100644 --- a/src/site.lisp +++ b/src/site.lisp @@ -20,9 +20,8 @@ #:template-vars #:load-theme #:theme) - (:import-from #:staticl/url - #:assert-absolute-url - #:object-url) + (:import-from #:staticl/utils + #:assert-absolute-url) (:import-from #:staticl/current-root #:with-current-root #:current-root) @@ -35,7 +34,8 @@ #:site-plugins #:site-theme #:site-pipeline - #:site-description)) + #:site-description + #:clean-urls-p)) (in-package #:staticl/site) @@ -64,6 +64,10 @@ :type string :reader site-url :documentation "Site's URL.") + (clean-urls :initarg :clean-urls + :type boolean + :reader clean-urls-p + :documentation "Generate some-page/index.html instead of some-page.html to make URLs look like https://my-site.com/some-page/ instead of https://my-site.com/some-page.html") (theme :initarg :theme :type theme :reader site-theme @@ -78,6 +82,7 @@ :description (error "DECRIPTION argument is required.") :navigation nil :url (error "URL argument is required.") + :clean-urls t :charset "UTF-8")) @@ -145,21 +150,19 @@ (values site)))) -(defmethod template-vars ((site site) &key (hash (dict))) +(defmethod template-vars ((site site) + (obj site) &key (hash (dict))) (dict* hash "title" - (site-title site) + (site-title obj) "description" - (site-description site) + (site-description obj) "url" - (site-url site) + (site-url obj) "pubdate" (local-time:now) "charset" - (site-charset site) + (site-charset obj) "navigation" - (site-navigation site))) - + (site-navigation obj))) -(defmethod object-url ((site site) &key &allow-other-keys) - (site-url site)) diff --git a/src/tag.lisp b/src/tag.lisp index f4d023e..6ca05a1 100644 --- a/src/tag.lisp +++ b/src/tag.lisp @@ -4,6 +4,8 @@ #:dict) (:import-from #:staticl/theme #:template-vars) + (:import-from #:staticl/site + #:site) (:export #:tag-name #:tag)) (in-package #:staticl/tag) @@ -17,7 +19,7 @@ :name (error ":NAME is required argument for a tag."))) -(defmethod template-vars ((tag tag) &key (hash (dict))) +(defmethod template-vars ((site site) (tag tag) &key (hash (dict))) (setf (gethash "name" hash) (tag-name tag)) (values hash)) diff --git a/src/theme.lisp b/src/theme.lisp index cd48cdf..0c38bec 100644 --- a/src/theme.lisp +++ b/src/theme.lisp @@ -29,7 +29,7 @@ (list (list :path "~S" (theme-path theme)))) -(defgeneric template-vars (object &key hash ) +(defgeneric template-vars (site object &key hash ) (:documentation "Fills a hash-table given as HASH argument with variables for filling a template. If hash is NIL, then a new hash-table should be allocated with EQUAL :TEST argument. @@ -77,15 +77,20 @@ (defun load-theme (name &key site-root) (let ((builtin-themes-dir (asdf:system-relative-pathname "staticl" - "themes"))) - (or (when site-root - (load-theme-from-dir site-root name)) + "themes")) + (site-themes-dir + (when site-root + (merge-pathnames + (make-pathname :directory '(:relative "themes")) + (uiop:ensure-directory-pathname site-root))))) + (or (when site-themes-dir + (load-theme-from-dir site-themes-dir name)) (load-theme-from-dir builtin-themes-dir name) (error "Theme named ~S not found in ~{~A~#[~; and ~:;, ~]~}" name (remove-if #'null - (list site-root + (list site-themes-dir builtin-themes-dir)))))) diff --git a/src/themes/closure-template.lisp b/src/themes/closure-template.lisp index acfcbe4..36f8229 100644 --- a/src/themes/closure-template.lisp +++ b/src/themes/closure-template.lisp @@ -47,12 +47,14 @@ (flet ((format-date (params end value) (declare (ignore params end)) - (format-timestring nil value - :format (date-format theme))) + (when value + (format-timestring nil value + :format (date-format theme)))) (format-datetime (params end value) (declare (ignore params end)) - (format-timestring nil value - :format (datetime-format theme)))) + (when value + (format-timestring nil value + :format (datetime-format theme))))) (register-print-handler :common-lisp-backend 'print-date :function #'format-date) diff --git a/src/url.lisp b/src/url.lisp index 2662d8b..fc5795b 100644 --- a/src/url.lisp +++ b/src/url.lisp @@ -6,17 +6,23 @@ (:import-from #:alexandria #:with-gensyms #:make-gensym) + (:import-from #:staticl/site + #:site) + (:import-from #:staticl/utils + #:assert-absolute-url + #:absolute-url-p) + (:import-from #:staticl/clean-urls + #:transform-url) (:export #:with-base-url - #:object-url - #:absolute-url-p)) + #:object-url)) (in-package #:staticl/url) (defvar *base-url*) -(defgeneric object-url (obj &key full) +(defgeneric object-url (site obj &key full) (:documentation "Returns a full object URL. A method should return an relative URL, but if case if FULL argument was given, the full url with schema and domain will be returned. @@ -31,7 +37,7 @@ Actually you will need to use FULL argument only in a rare case when you really need and absolute URL, for example in an RSS feed.") - (:method :around ((obj t) &key full) + (:method :around ((site site) (obj t) &key full) (let* ((result (call-next-method)) (absolute-url (cond @@ -43,36 +49,13 @@ result)) (quri:merge-uris result *base-url*))))) - (cond - (full - (quri:render-uri - absolute-url)) - (t - (quri:uri-path absolute-url)))))) - - -(-> absolute-url-p (string) - (values boolean &optional)) - -(defun absolute-url-p (url) - (let ((parsed (quri:uri url))) - (when (and (quri:uri-scheme parsed) - (quri:uri-host parsed)) - (values t)))) - - -(-> assert-absolute-url (string) - (values string &optional)) - -(defun assert-absolute-url (url) - (let ((parsed (quri:uri url))) - (unless (quri:uri-scheme parsed) - (error "There is no scheme in ~S." - url)) - (unless (quri:uri-host parsed) - (error "There is no host in ~S." - url)) - url)) + (transform-url site + (cond + (full + (quri:render-uri + absolute-url)) + (t + (quri:uri-path absolute-url))))))) (defun call-with-base-url (url thunk) diff --git a/src/user-package.lisp b/src/user-package.lisp index a7a53e5..899c7b2 100644 --- a/src/user-package.lisp +++ b/src/user-package.lisp @@ -1,14 +1,17 @@ (uiop:define-package #:staticl-user ;; This package does not use all symbols from CL package intentionally: - (:use) + (:use #:cl) + (:nicknames #:staticl/user-package) - (:import-from #:cl - #:list - #:t - #:nil - #:lambda - #:let - #:in-package) + ;; (:import-from #:cl + ;; #:list + ;; #:t + ;; #:nil + ;; #:lambda + ;; #:let + ;; #:in-package + ;; #:defpackage) + (:import-from #:serapeum #:fmt) ;; API imports @@ -25,8 +28,8 @@ #:load-content) (:import-from #:staticl/feeds/rss #:rss) - (:import-from #:staticl/feeds/atom - #:atom) + (:shadowing-import-from #:staticl/feeds/atom + #:atom) (:import-from #:staticl/filter #:filter) (:import-from #:staticl/rsync diff --git a/src/utils.lisp b/src/utils.lisp index 97259da..c00353b 100644 --- a/src/utils.lisp +++ b/src/utils.lisp @@ -1,6 +1,7 @@ (uiop:define-package #:staticl/utils (:use #:cl) (:import-from #:log) + (:import-from #:quri) (:import-from #:str #:trim-left) (:import-from #:serapeum @@ -14,7 +15,9 @@ #:walk-directory) (:export #:do-files - #:normalize-plist)) + #:normalize-plist + #:absolute-url-p + #:assert-absolute-url)) (in-package #:staticl/utils) @@ -153,3 +156,27 @@ BODY on files that match the given extension." (declare (dynamic-extent #'rec #'to-upper)) (rec dict))) + + +(-> absolute-url-p (string) + (values boolean &optional)) + +(defun absolute-url-p (url) + (let ((parsed (quri:uri url))) + (when (and (quri:uri-scheme parsed) + (quri:uri-host parsed)) + (values t)))) + + +(-> assert-absolute-url (string) + (values string &optional)) + +(defun assert-absolute-url (url) + (let ((parsed (quri:uri url))) + (unless (quri:uri-scheme parsed) + (error "There is no scheme in ~S." + url)) + (unless (quri:uri-host parsed) + (error "There is no host in ~S." + url)) + url)) diff --git a/staticl.asd b/staticl.asd index 9e27f35..9e1936e 100644 --- a/staticl.asd +++ b/staticl.asd @@ -15,7 +15,8 @@ "staticl/themes/closure-template" "staticl/format/html" "staticl/format/md" - "staticl/user-package") + "staticl/user-package" + "staticl/site-url") :in-order-to ((test-op (test-op "staticl-tests"))))