From 196fee86066b38f60a8e109a1175e78b349b5cca Mon Sep 17 00:00:00 2001 From: Alexander Artemenko Date: Mon, 29 Apr 2024 18:26:32 +0300 Subject: [PATCH] Added tags-index node. --- docs/difference-from-coleslaw.lisp | 4 + src/content.lisp | 3 +- src/index/paginated.lisp | 4 +- src/index/tags.lisp | 157 +++++++++++++++++++++++++++++ src/user-package.lisp | 13 +-- 5 files changed, 167 insertions(+), 14 deletions(-) create mode 100644 src/index/tags.lisp diff --git a/docs/difference-from-coleslaw.lisp b/docs/difference-from-coleslaw.lisp index 888c098..cd3aeeb 100644 --- a/docs/difference-from-coleslaw.lisp +++ b/docs/difference-from-coleslaw.lisp @@ -17,6 +17,10 @@ For index pages a list of items was also moved and now instead of `index.content 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). +### Index by tag + +Coleslaw always rendered pages where posts are grouped by tags. But with Staticl you have to include TAGS-INDEX function call into the site's pipeline. Without this step, tag objects will not have a \"url\" slot and template might be ready to render tags without the URL. + ## Other field renames - `pubdate -> site.pubdate` diff --git a/src/content.lisp b/src/content.lisp index 654ea3b..4392c83 100644 --- a/src/content.lisp +++ b/src/content.lisp @@ -68,7 +68,8 @@ #:content-title #:content-excerpt-separator #:set-metadata - #:load-content)) + #:load-content + #:content-tags)) (in-package #:staticl/content) diff --git a/src/index/paginated.lisp b/src/index/paginated.lisp index 659ed71..626e180 100644 --- a/src/index/paginated.lisp +++ b/src/index/paginated.lisp @@ -12,9 +12,7 @@ #:fmt) (:import-from #:staticl/content/post #:postp) - (:export #:paginated-index - #:make-page-title - #:make-page-filename)) + (:export #:paginated-index)) (in-package #:staticl/index/paginated) diff --git a/src/index/tags.lisp b/src/index/tags.lisp new file mode 100644 index 0000000..5f56049 --- /dev/null +++ b/src/index/tags.lisp @@ -0,0 +1,157 @@ +(uiop:define-package #:staticl/index/tags + (:use #:cl) + (:import-from #:staticl/index/base + #:page-size + #:index-target-path + #:index-page + #:base-index) + (:import-from #:staticl/index/base + #:base-index) + (:import-from #:serapeum + #:-> + #:dict + #:fmt) + (:import-from #:alexandria + #:curry + #:rcurry) + (:import-from #:staticl/content + #:content-with-title-mixin + #:content-title + #:content-tags + #:content-with-tags-mixin) + (:import-from #:staticl/tag + #:tag + #:tag-name) + (:import-from #:staticl/site + #:site) + (:import-from #:staticl/theme + #:template-vars) + (:import-from #:staticl/url + #:object-url)) +(in-package #:staticl/index/tags) + + +(deftype function-from-string-to-string () + '(function (string) (values string &optional))) + + +(deftype function-from-string-to-pathname () + '(function (string) (values pathname &optional))) + + +(declaim (ftype function-from-string-to-string + default-page-title-fn)) + +(defun default-page-title-fn (tag-name) + (fmt "Posts with tag \"~A\"" tag-name)) + + +(declaim (ftype function-from-string-to-pathname + default-page-filename-fn)) + +(defun default-page-filename-fn (tag-name) + (make-pathname :name tag-name + :type "html")) + + + +(defclass tags-index (base-index) + ((page-filename-fn :initarg :page-filename-fn + :type (or null function-from-string-to-pathname) + :documentation "A callback to change page titles. + + Accepts single argument - a tag name and should return a pathname + + By default, for tag \"foo-bar\" it returns foo-bar.html. + + If site has \"clean urls\" setting enabled, then additional + transformation to the pathname will be + applied automatically." + :reader page-filename-fn) + (page-title-fn :initarg :page-title-fn + :type (or null function-from-string-to-string) + :documentation "A callback to change page titles. + + Accepts single argument - a tag name and should return a string. + + For example, here is how you can translate page title into a russian: + + ```lisp + (tags-index :target-path #P\"ru/\" + :page-title-fn (lambda (tag-name) + (fmt \"Записи с тегом ~A\" tag-name))) + ``` + " + :reader page-title-fn)) + (:default-initargs + :page-title-fn #'default-page-title-fn + :page-filename-fn #'default-page-filename-fn)) + + +(defun tags-index (&rest initargs &key target-path page-size template page-title-fn page-filename-fn) + (declare (ignore target-path page-size template page-title-fn page-filename-fn)) + (apply #'make-instance 'tags-index + initargs)) + + +(defclass bound-tag (tag) + ((index-page :initarg :index-page + :reader tag-index-page)) + (:documentation "A tag bound to the index page. A URL for such tags will lead to the index page.")) + + +(-> upgrade-tag (string index-page content-with-tags-mixin) + (values &optional)) + +(defun upgrade-tag (tag-name index-page content) + (loop for tag in (content-tags content) + when (string= (tag-name tag) + tag-name) + do (change-class tag 'bound-tag + :index-page index-page)) + (values)) + + +(defmethod template-vars ((site site) (tag bound-tag) &key (hash (dict))) + (let ((hash (call-next-method site tag :hash hash))) + (setf (gethash "url" hash) + (object-url site (tag-index-page tag))) + (values hash))) + + +(defmethod staticl/pipeline:process-items ((site site) (index tags-index) content-items) + "Here we are grouping all posts by their tags, generate an index page for each tag and upgrade tag instances to the tags bound to their corresponding index pages. + + Posts on index page are sorted by their titles." + + (loop with only-posts = (remove-if-not (lambda (item) + (and (typep item 'content-with-tags-mixin) + (typep item 'content-with-title-mixin))) + content-items) + with by-tag = (dict) + for post in only-posts + do (loop for tag in (content-tags post) + do (push post + (gethash (tag-name tag) by-tag))) + finally (loop for tag-name being the hash-key of by-tag + using (hash-value tagged-posts) + for sorted-posts = (sort tagged-posts #'string< + :key #'content-title) + for index-page = (make-instance 'index-page + :title (funcall (page-title-fn index) + tag-name) + :target-path (merge-pathnames + ;; TODO: implement clean urls + (funcall (page-filename-fn index) + tag-name) + (uiop:ensure-directory-pathname + (index-target-path index))) + :items sorted-posts) + do (staticl/pipeline:produce-item index-page) + ;; Here we change tag classes, to make them + ;; render as links to the corresponding index page: + (mapc (curry #'upgrade-tag + tag-name + index-page) + sorted-posts))) + (values)) diff --git a/src/user-package.lisp b/src/user-package.lisp index 899c7b2..5287d4d 100644 --- a/src/user-package.lisp +++ b/src/user-package.lisp @@ -1,16 +1,7 @@ (uiop:define-package #:staticl-user ;; This package does not use all symbols from CL package intentionally: (:use #:cl) - (:nicknames #:staticl/user-package) - ;; (:import-from #:cl - ;; #:list - ;; #:t - ;; #:nil - ;; #:lambda - ;; #:let - ;; #:in-package - ;; #:defpackage) (:import-from #:serapeum #:fmt) @@ -35,4 +26,6 @@ (:import-from #:staticl/rsync #:rsync) (:import-from #:staticl/index/paginated - #:paginated-index)) + #:paginated-index) + (:import-from #:staticl/index/tags + #:tags-index))