diff --git a/docs/plugin-use.md b/docs/plugin-use.md index fba2d6e..2337a31 100644 --- a/docs/plugin-use.md +++ b/docs/plugin-use.md @@ -39,6 +39,34 @@ **Example**: `(gh-pages :cname t)` +## SEO and Social Metadata + +**Description**: Adds description and keywords metadata for SEO purposes. + Adds both Open Graph and Twitter Cards metadata for sharing posts as well. + `:twitter` keyword argument sets "site" property of Twitter. + Twitter Card specific metadata will be omitted if this property is not set. + `:card` keyword argument sets the type of Twitter card created. + Possible types are `:summary` and `:image`. Defaults to `:summary`. + + Five *optional* tags for post file header are added. + + `keywords:` comma, seperated, seo, keywords. + They will be generated from tags if empty. + + `description:` Description to be used in SEO and + Open Graph description tags. If empty, Open Graph description will be generated + from content while SEO description metadata will be omitted. + + `image:` either an absolute (`http://www.example.com/image.png`) + or a root-relative (`/static/image.png`) image URL. + + `card:` Overrides Twitter Card type defined in plugin activation. + Possible values are either `image` or `summary`. + + `creator:` Twitter username of the content creator. + +**Example**: `(metadata :twitter "twitter_account" :card :summary)` + ## Incremental Builds **Description**: Primarily a performance enhancement. Caches the diff --git a/plugins/metadata.lisp b/plugins/metadata.lisp new file mode 100644 index 0000000..efb82b9 --- /dev/null +++ b/plugins/metadata.lisp @@ -0,0 +1,81 @@ +(eval-when (:compile-toplevel :load-toplevel) + (ql:quickload 's-xml)) + +(defpackage :coleslaw-metadata + (:use :cl :coleslaw) + (:import-from :coleslaw + #:title + #:domain + #:tag-name + #:page-url + #:content-tags + #:content-text + #:keywords-of + #:description-of + #:image-of + #:card-format + #:creator-of) + (:import-from :s-xml + #:xml-parser-state + #:start-parse-xml) + (:export #:enable)) + +(in-package :coleslaw-metadata) + +(defparameter *description-length* 200) + +(defvar *metadata-header* + " +~@[ +~]~:[~*~*~;~:* + +~@[ +~]~] + + + +~@[ +~] +") + +(defun remove-markup (text) + (with-input-from-string (in text) + (let* ((state (make-instance 'xml-parser-state + :text-hook #'(lambda (string seed) (cons string seed)))) + (result (start-parse-xml in state))) + (apply #'concatenate 'string (nreverse result))))) + +(defun shorten-text (text) + (if (< *description-length* (length text)) + (subseq text 0 (- *description-length* 1)) text)) + +(defun compile-description (text) + (shorten-text (remove #\" (remove-markup text)))) + +(defun root-relative-url-p (url) + (eq (elt url 0) #\/)) + +(defun compile-url (url) + (if (root-relative-url-p url) + (concatenate 'string (domain *config*) url) + url)) + +(defun compile-metadata (post twitter card) + (format nil *metadata-header* + (or (keywords-of post) + (format nil "~{~A~^, ~}" (mapcar #'tag-name (content-tags post)))) + (description-of post) + twitter + (eq (or (card-format post) card) :image) + (creator-of post) + (title-of post) + (title *config*) + (concatenate 'string (domain *config*) "/" (namestring (page-url post))) + (or (description-of post) + (compile-description (content-text post))) + (when (image-of post) (compile-url (image-of post))))) + +(defun enable (&key twitter card) + (flet ((inject-p (x) + (when (typep x 'post) (compile-metadata x twitter card)))) + (add-injection #'inject-p :head))) diff --git a/src/posts.lisp b/src/posts.lisp index d8fe29d..71d8100 100644 --- a/src/posts.lisp +++ b/src/posts.lisp @@ -3,15 +3,27 @@ (defclass post (content) ((title :initarg :title :reader title-of) (author :initarg :author :reader author-of) - (format :initarg :format :reader post-format)) - (:default-initargs :author nil)) + (format :initarg :format :reader post-format) + (keywords :initarg :keywords :reader keywords-of) + (description :initarg :description :reader description-of) + (image :initarg :image :reader image-of) + (card :initarg :card :reader card-format) + (creator :initarg :creator :reader creator-of)) + (:default-initargs + :author nil + :keywords nil + :description nil + :image nil + :card nil + :creator nil)) (defmethod initialize-instance :after ((object post) &key) - (with-slots (url title author format text) object + (with-slots (url title author format card text) object (setf url (compute-url object (slugify title)) format (make-keyword (string-upcase format)) text (render-text text format) - author (or author (author *config*))))) + author (or author (author *config*)) + card (if card (make-keyword (string-upcase card)))))) (defmethod render ((object post) &key prev next) (funcall (theme-fn 'post) (list :config *config*