◊(Local Yarn Code "Check-in [cb8af9fc]")

Overview
Comment:Split cache schema and fetchers into a separate module
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: cb8af9fcd70a9471978978ac9be749a652bbd16780a0a2bac718817646a06ab2
User & Date: joel on 2020-02-02 04:20:23
Other Links: manifest | tags
Context
2020-02-02
04:53
Include bold type check-in: 4fecf9d9 user: joel tags: trunk
04:20
Split cache schema and fetchers into a separate module check-in: cb8af9fc user: joel tags: trunk
2020-02-01
22:54
Remove filter property on home page image, closes [0d5932bd9996832b] check-in: 1129e752 user: joel tags: trunk
Changes

Modified blog.rkt from [abf461d1] to [75b9d900].

     2      2   
     3      3   ; SPDX-License-Identifier: BlueOak-1.0.0
     4      4   ; This file is licensed under the Blue Oak Model License 1.0.0.
     5      5   
     6      6   ;; Builds the paginated “blog” HTML files (blog-pg1.html ...) from the SQLite cache
     7      7   ;; The files will be written out every time this module is evaluated! (see end)
     8      8   
     9         -(require "crystalize.rkt"
            9  +(require "cache.rkt"
    10     10            "snippets-html.rkt"
    11     11            "dust.rkt"
    12     12            racket/file
    13     13            sugar/list)
    14     14   
    15     15   (provide main)
    16     16   

Added cache.rkt version [7fc7c05e].

            1  +#lang racket/base
            2  +
            3  +; SPDX-License-Identifier: BlueOak-1.0.0
            4  +; This file is licensed under the Blue Oak Model License 1.0.0.
            5  +
            6  +(require deta
            7  +         db/base
            8  +         db/sqlite3
            9  +         threading
           10  +         pollen/setup
           11  +         racket/match
           12  +         "dust.rkt"
           13  +         (except-in pollen/core select))
           14  +
           15  +(provide init-cache-db!
           16  +         cache-conn                     ; The most eligible bachelor in Neo Yokyo
           17  +         (schema-out cache:article)
           18  +         (schema-out cache:note)
           19  +         (schema-out cache:series)
           20  +         (schema-out cache:index-entry)
           21  +         delete-article!
           22  +         delete-notes!
           23  +         current-plain-title
           24  +         articles
           25  +         articles+notes
           26  +         listing-htmls
           27  +         <listing-full>
           28  +         <listing-excerpt>
           29  +         <listing-short>
           30  +         unfence)
           31  +
           32  +;; Cache DB and Schemas
           33  +
           34  +(define DBFILE (build-path (current-project-root) "vitreous.sqlite"))
           35  +(define cache-conn (sqlite3-connect #:database DBFILE #:mode 'create))
           36  +
           37  +(define current-plain-title (make-parameter "void"))
           38  +
           39  +(define-schema cache:article #:table "articles"
           40  +  ([id                   id/f #:primary-key #:auto-increment]
           41  +   [page                 symbol/f]
           42  +   [title-plain          string/f]
           43  +   [title-html-flow      string/f]
           44  +   [title-specified?     boolean/f]
           45  +   [published            string/f]
           46  +   [updated              string/f]
           47  +   [author               string/f]
           48  +   [conceal              string/f]
           49  +   [series-page          symbol/f]
           50  +   [noun-singular        string/f]
           51  +   [note-count           integer/f]
           52  +   [doc-html             string/f]
           53  +   [disposition          string/f]
           54  +   [disp-html-anchor     string/f]
           55  +   [listing-full-html    string/f]   ; full content but without notes
           56  +   [listing-excerpt-html string/f]   ; Not used for now
           57  +   [listing-short-html   string/f])) ; Date and title only
           58  +
           59  +(define-schema cache:note #:table "notes"
           60  +  ([id                   id/f #:primary-key #:auto-increment]
           61  +   [page                 symbol/f]
           62  +   [html-anchor          string/f]
           63  +   [title-html-flow      string/f] ; No block-level HTML elements
           64  +   [title-plain          string/f]
           65  +   [author               string/f]
           66  +   [author-url           string/f]
           67  +   [published            string/f]
           68  +   [disposition          string/f]
           69  +   [content-html         string/f]
           70  +   [series-page          symbol/f]
           71  +   [conceal              string/f]
           72  +   [listing-full-html    string/f]
           73  +   [listing-excerpt-html string/f]   ; Not used for now
           74  +   [listing-short-html   string/f])) ; Date and title only
           75  +
           76  +(define-schema cache:series #:table "series"
           77  +  ([id            id/f #:primary-key #:auto-increment]
           78  +   [page          symbol/f]
           79  +   [title         string/f]
           80  +   [published     string/f]
           81  +   [noun-plural   string/f]
           82  +   [noun-singular string/f]))
           83  +
           84  +(define-schema cache:index-entry #:table "index_entries"
           85  +  ([id          id/f #:primary-key #:auto-increment]
           86  +   [entry       string/f]
           87  +   [subentry    string/f]
           88  +   [page        symbol/f]
           89  +   [html-anchor string/f]))
           90  +
           91  +(define-schema listing
           92  +  #:virtual
           93  +  ([html        string/f]
           94  +   [published   date/f]
           95  +   [series-page symbol/f]))
           96  +
           97  +(define (init-cache-db!)
           98  +  (create-table! cache-conn 'cache:article)
           99  +  (create-table! cache-conn 'cache:note)
          100  +  (create-table! cache-conn 'cache:series)
          101  +  (create-table! cache-conn 'cache:index-entry))
          102  +
          103  +(define (delete-article! page)
          104  +  (query-exec cache-conn
          105  +              (~> (from cache:article #:as a)
          106  +                  (where (= a.page ,(format "~a" page)))
          107  +                  delete)))
          108  +
          109  +(define (delete-notes! page)
          110  +  (query-exec cache-conn
          111  +              (~> (from cache:note #:as n)
          112  +                  (where (= n.page ,(format "~a" page)))
          113  +                  delete)))
          114  +
          115  +;;
          116  +;;  ~~~ Fetching articles and notes ~~~
          117  +;;
          118  +
          119  +;; (Private use) Conveniece function for the WHERE `series-page` clause
          120  +(define (where-series q s)
          121  +  (define (s->p x) (format "~a/~a.html" series-folder x))
          122  +  (match s
          123  +    [(list series ...)
          124  +     (where q (in a.series-page ,(map s->p series)))] ; WHERE series-page IN (item1 ...)
          125  +    [(or (? string? series) (? symbol? series))
          126  +     (where q (= a.series-page ,(s->p series)))]      ; WHERE series-page = "item"
          127  +    [#t
          128  +     (where q (= a.series-page ,(path->string (here-output-path))))]
          129  +    [_ q]))
          130  +
          131  +;; (Private use) Convenience for the WHERE `conceal` NOT LIKE clause
          132  +(define (where-not-concealed q)
          133  +  (define base-clause (where q (not (like a.conceal "%all%"))))
          134  +  (match (listing-context)
          135  +    ["" base-clause]
          136  +    [(var context) (where base-clause (not (like a.conceal ,(format "%~a%" context))))]))
          137  +
          138  +;; Needed to "parameterize" column names
          139  +;; see https://github.com/Bogdanp/deta/issues/14#issuecomment-573344928
          140  +(require (prefix-in ast: deta/private/ast))
          141  +
          142  +;; Builds a query to fetch articles
          143  +(define (articles type #:series [s #t] #:limit [lim -1] #:order [ord 'desc])
          144  +  (define html-field (format "listing_~a_html" type))
          145  +  (~> (from cache:article #:as a)
          146  +      (select (fragment (ast:as (ast:qualified "a" html-field) "html"))
          147  +              a.published
          148  +              a.series-page
          149  +              a.conceal)
          150  +      (where-series s)
          151  +      (where-not-concealed)
          152  +      (limit ,lim)
          153  +      (order-by ([a.published ,ord]))
          154  +      (project-onto listing-schema)))
          155  +
          156  +;; Builds a query that returns articles and notes intermingled chronologically
          157  +(define (articles+notes type #:series [s #t] #:limit [lim -1] #:order [ord 'desc])
          158  +  (define html-field (format "listing_~a_html" type))
          159  +  (~> (from (subquery
          160  +             (~> (from cache:article #:as A)
          161  +                 (select (fragment (ast:as (ast:qualified "A" html-field) "html"))
          162  +                         A.published
          163  +                         A.series-page
          164  +                         A.conceal)
          165  +                 (union
          166  +                  (~> (from cache:note #:as N)
          167  +                      (select (fragment (ast:as (ast:qualified "N" html-field) "html"))
          168  +                              N.published
          169  +                              N.series-page
          170  +                              N.conceal)))))
          171  +            #:as a)
          172  +      (where-series s)
          173  +      (where-not-concealed)
          174  +      (limit ,lim)
          175  +      (order-by ([a.published ,ord]))
          176  +      (project-onto listing-schema)))
          177  +
          178  +;; Get all the a list of the HTML all the results in a query
          179  +(define (listing-htmls list-query)
          180  +  (for/list ([l (in-entities cache-conn list-query)])
          181  +    (listing-html l)))
          182  +
          183  +;; Return cached HTML of articles and/or notes, fenced within a style txexpr to prevent it being
          184  +;; escaped by ->html. See also: definition of `unfence`
          185  +
          186  +;; E.g.: (<listing-full> articles+notes)
          187  +(define (<listing-full> query-func #:series [s #t] #:limit [lim -1] #:order [ord 'desc])
          188  +  `(style ,@(listing-htmls (query-func 'full #:series s #:limit lim #:order ord))))
          189  +;;                                     ^^^^^
          190  +
          191  +(define (<listing-excerpt> query-func #:series [s #t] #:limit [lim -1] #:order [ord 'desc])
          192  +  `(style ,@(listing-htmls (query-func 'excerpt #:series s #:limit lim #:order ord))))
          193  +;;                                     ^^^^^^^^
          194  +
          195  +(define (<listing-short> query-func #:series [s #t] #:limit [lim -1] #:order [ord 'desc])
          196  +  `(style "<ul class=\"article-list\">"
          197  +          ,@(listing-htmls (query-func 'short #:series s #:limit lim #:order ord))
          198  +          "</ul>")) ;;                 ^^^^^^
          199  +
          200  +;; Remove "<style>" and "</style>" introduced by using ->html on docs containing output from
          201  +;; listing functions
          202  +(define (unfence html-str)
          203  +  (regexp-replace* #px"<[\\/]{0,1}style>" html-str ""))

Modified crystalize.rkt from [f45f7570] to [c41782ea].

     1      1   #lang racket/base
     2      2   
     3      3   ; SPDX-License-Identifier: BlueOak-1.0.0
     4      4   ; This file is licensed under the Blue Oak Model License 1.0.0.
     5      5   
     6         -(require deta db/base db/sqlite3 threading txexpr gregor)
     7         -
     8         -(require racket/match
            6  +(require deta
            7  +         db/base
            8  +         db/sqlite3
            9  +         threading
           10  +         racket/match
     9     11            racket/string
    10         -         pollen/pagetree
           12  +         txexpr
    11     13            pollen/template
    12     14            (except-in pollen/core select) ; avoid conflict with deta
    13         -         pollen/setup)
    14         -
    15         -(require "dust.rkt" "snippets-html.rkt")
    16         -
    17         -(provide init-cache-db!
    18         -         cache-conn            ; The most eligible bachelor in Neo Yokyo
    19         -         parse-and-cache-article!
    20         -         cache-series!
    21         -         current-plain-title
    22         -         (schema-out cache:article)
    23         -         (schema-out cache:note)
    24         -         (schema-out cache:series)
    25         -         (schema-out cache:index-entry)
    26         -         articles
    27         -         articles+notes
    28         -         listing-htmls
    29         -         <listing-full>
    30         -         <listing-excerpt>
    31         -         <listing-short>
    32         -         unfence)
    33         -
    34         -;; Cache DB and Schemas
    35         -
    36         -(define DBFILE (build-path (current-project-root) "vitreous.sqlite"))
    37         -(define cache-conn (sqlite3-connect #:database DBFILE #:mode 'create))
    38         -
    39         -(define current-plain-title (make-parameter "void"))
    40         -
    41         -(define-schema cache:article #:table "articles"
    42         -  ([id                   id/f #:primary-key #:auto-increment]
    43         -   [page                 symbol/f]
    44         -   [title-plain          string/f]
    45         -   [title-html-flow      string/f]
    46         -   [title-specified?     boolean/f]
    47         -   [published            string/f]
    48         -   [updated              string/f]
    49         -   [author               string/f]
    50         -   [conceal              string/f]
    51         -   [series-page          symbol/f]
    52         -   [noun-singular        string/f]
    53         -   [note-count           integer/f]
    54         -   [doc-html             string/f]
    55         -   [disposition          string/f]
    56         -   [disp-html-anchor     string/f]
    57         -   [listing-full-html    string/f]   ; full content but without notes
    58         -   [listing-excerpt-html string/f]   ; Not used for now
    59         -   [listing-short-html   string/f])) ; Date and title only
    60         -
    61         -(define-schema cache:note #:table "notes"
    62         -  ([id                   id/f #:primary-key #:auto-increment]
    63         -   [page                 symbol/f]
    64         -   [html-anchor          string/f]
    65         -   [title-html-flow      string/f] ; No block-level HTML elements
    66         -   [title-plain          string/f]
    67         -   [author               string/f]
    68         -   [author-url           string/f]
    69         -   [published            string/f]
    70         -   [disposition          string/f]
    71         -   [content-html         string/f]
    72         -   [series-page          symbol/f]
    73         -   [conceal              string/f]
    74         -   [listing-full-html    string/f]
    75         -   [listing-excerpt-html string/f]   ; Not used for now
    76         -   [listing-short-html   string/f])) ; Date and title only
    77         -
    78         -(define-schema cache:series #:table "series"
    79         -  ([id            id/f #:primary-key #:auto-increment]
    80         -   [page          symbol/f]
    81         -   [title         string/f]
    82         -   [published     string/f]
    83         -   [noun-plural   string/f]
    84         -   [noun-singular string/f]))
    85         -
    86         -(define-schema cache:index-entry #:table "index_entries"
    87         -  ([id          id/f #:primary-key #:auto-increment]
    88         -   [entry       string/f]
    89         -   [subentry    string/f]
    90         -   [page        symbol/f]
    91         -   [html-anchor string/f]))
    92         -
    93         -(define-schema listing
    94         -  #:virtual
    95         -  ([html        string/f]
    96         -   [published   date/f]
    97         -   [series-page symbol/f]))
    98         -
    99         -(define (init-cache-db!)
   100         -  (create-table! cache-conn 'cache:article)
   101         -  (create-table! cache-conn 'cache:note)
   102         -  (create-table! cache-conn 'cache:series)
   103         -  (create-table! cache-conn 'cache:index-entry))
   104         -
   105         -(define (delete-article! page)
   106         -  (query-exec cache-conn
   107         -              (~> (from cache:article #:as a)
   108         -                  (where (= a.page ,(format "~a" page)))
   109         -                  delete)))
   110         -
   111         -(define (delete-notes! page)
   112         -  (query-exec cache-conn
   113         -              (~> (from cache:note #:as n)
   114         -                  (where (= n.page ,(format "~a" page)))
   115         -                  delete)))
   116         -
           15  +)
           16  +
           17  +(require "dust.rkt" "cache.rkt" "snippets-html.rkt")
           18  +
           19  +(provide parse-and-cache-article!
           20  +         cache-series!)
   117     21   
   118     22   ;; Save an article and its notes (if any) to the database, and return the
   119     23   ;; rendered HTML of the complete article.
   120         -;;
   121     24   (define (parse-and-cache-article! pagenode doc)
   122     25     (define-values (doc-no-title maybe-title)
   123     26       (splitf-txexpr doc (make-tag-predicate 'title)))
   124     27     (define-values (body-txpr note-txprs)
   125     28       (splitf-txexpr doc-no-title (make-tag-predicate 'note)))
   126     29     (define-values (disposition disp-note-id)
   127     30       (notes->last-disposition-values note-txprs))
................................................................................
   325    228                                        (where (= entry.page ,(symbol->string pagenode))))))
   326    229     (unless (null? entry-txs)
   327    230       (void
   328    231        (apply insert! cache-conn
   329    232               (for/list ([etx (in-list entry-txs)])
   330    233                 (txexpr->index-entry etx pagenode))))))
   331    234   
   332         -;;
   333         -;;  ~~~ Fetching articles and notes ~~~
   334         -;;
   335         -
   336         -;; (Private use) Conveniece function for the WHERE `series-page` clause
   337         -(define (where-series q s)
   338         -  (define (s->p x) (format "~a/~a.html" series-folder x))
   339         -  (match s
   340         -    [(list series ...)
   341         -     (where q (in a.series-page ,(map s->p series)))] ; WHERE series-page IN (item1 ...)
   342         -    [(or (? string? series) (? symbol? series))
   343         -     (where q (= a.series-page ,(s->p series)))]      ; WHERE series-page = "item"
   344         -    [#t
   345         -     (where q (= a.series-page ,(path->string (here-output-path))))]
   346         -    [_ q]))
   347         -
   348         -;; (Private use) Convenience for the WHERE `conceal` NOT LIKE clause
   349         -(define (where-not-concealed q)
   350         -  (define base-clause (where q (not (like a.conceal "%all%"))))
   351         -  (match (listing-context)
   352         -    ["" base-clause]
   353         -    [(var context) (where base-clause (not (like a.conceal ,(format "%~a%" context))))]))
   354         -
   355         -;; Needed to "parameterize" column names
   356         -;; see https://github.com/Bogdanp/deta/issues/14#issuecomment-573344928
   357         -(require (prefix-in ast: deta/private/ast))
   358         -
   359         -;; Builds a query to fetch articles
   360         -(define (articles type #:series [s #t] #:limit [lim -1] #:order [ord 'desc])
   361         -  (define html-field (format "listing_~a_html" type))
   362         -  (~> (from cache:article #:as a)
   363         -      (select (fragment (ast:as (ast:qualified "a" html-field) "html"))
   364         -              a.published
   365         -              a.series-page
   366         -              a.conceal)
   367         -      (where-series s)
   368         -      (where-not-concealed)
   369         -      (limit ,lim)
   370         -      (order-by ([a.published ,ord]))
   371         -      (project-onto listing-schema)))
   372         -
   373         -;; Builds a query that returns articles and notes intermingled chronologically
   374         -(define (articles+notes type #:series [s #t] #:limit [lim -1] #:order [ord 'desc])
   375         -  (define html-field (format "listing_~a_html" type))
   376         -  (~> (from (subquery
   377         -             (~> (from cache:article #:as A)
   378         -                 (select (fragment (ast:as (ast:qualified "A" html-field) "html"))
   379         -                         A.published
   380         -                         A.series-page
   381         -                         A.conceal)
   382         -                 (union
   383         -                  (~> (from cache:note #:as N)
   384         -                      (select (fragment (ast:as (ast:qualified "N" html-field) "html"))
   385         -                              N.published
   386         -                              N.series-page
   387         -                              N.conceal)))))
   388         -            #:as a)
   389         -      (where-series s)
   390         -      (where-not-concealed)
   391         -      (limit ,lim)
   392         -      (order-by ([a.published ,ord]))
   393         -      (project-onto listing-schema)))
   394         -
   395         -;; Get all the a list of the HTML all the results in a query
   396         -(define (listing-htmls list-query)
   397         -  (for/list ([l (in-entities cache-conn list-query)])
   398         -    (listing-html l)))
   399         -
   400         -;; Return cached HTML of articles and/or notes, fenced within a style txexpr to prevent it being
   401         -;; escaped by ->html. See also: definition of `unfence`
   402         -
   403         -;; E.g.: (<listing-full> articles+notes)
   404         -(define (<listing-full> query-func #:series [s #t] #:limit [lim -1] #:order [ord 'desc])
   405         -  `(style ,@(listing-htmls (query-func 'full #:series s #:limit lim #:order ord))))
   406         -;;                                     ^^^^^
   407         -
   408         -(define (<listing-excerpt> query-func #:series [s #t] #:limit [lim -1] #:order [ord 'desc])
   409         -  `(style ,@(listing-htmls (query-func 'excerpt #:series s #:limit lim #:order ord))))
   410         -;;                                     ^^^^^^^^
   411         -
   412         -(define (<listing-short> query-func #:series [s #t] #:limit [lim -1] #:order [ord 'desc])
   413         -  `(style "<ul class=\"article-list\">"
   414         -          ,@(listing-htmls (query-func 'short #:series s #:limit lim #:order ord))
   415         -          "</ul>")) ;;                 ^^^^^^
   416         -
   417         -;; Remove "<style>" and "</style>" introduced by using ->html on docs containing output from
   418         -;; listing functions
   419         -(define (unfence html-str)
   420         -  (regexp-replace* #px"<[\\/]{0,1}style>" html-str ""))
   421    235   
   422    236   ;; Save the current article to the `series` table of the SQLite cache
   423    237   ;; Should be called from a template for series pages
   424    238   (define (cache-series!)
   425    239     (define here-page (path->string (here-output-path)))
   426    240     (query-exec cache-conn
   427    241                 (delete (~> (from cache:series #:as s)

Modified keyword-index.rkt from [50258609] to [d37c4752].

    10     10            racket/list
    11     11            racket/file
    12     12            racket/string
    13     13            db/base
    14     14            net/uri-codec
    15     15            pollen/template)
    16     16   
    17         -(require "crystalize.rkt"
           17  +(require "cache.rkt"
    18     18            "dust.rkt"
    19     19            "snippets-html.rkt")
    20     20   
    21     21   (provide main)
    22     22   
    23     23   ;; Terminology (because these things get confusing fast)
    24     24   ;;

Modified makefile from [ceed52b6] to [223b34de].

     3      3   
     4      4   SHELL = /bin/bash
     5      5   
     6      6   # ~~~ Variables used by rules ~~~
     7      7   #
     8      8   
     9      9   core-files := pollen.rkt dust.rkt
    10         -html-deps  := snippets-html.rkt tags-html.rkt crystalize.rkt
           10  +html-deps  := snippets-html.rkt tags-html.rkt crystalize.rkt cache.rkt
    11     11   
    12     12   article-sources := $(wildcard articles/*.poly.pm)
    13     13   articles-html   := $(patsubst %.poly.pm, %.html, $(article-sources))
    14     14   articles-pdf    := $(patsubst %.poly.pm, %.pdf,  $(article-sources))
    15     15   
    16     16   series-sources  := $(wildcard series/*.poly.pm)
    17     17   series-html     := $(patsubst %.poly.pm, %.html, $(series-sources))

Modified pollen.rkt from [ec6e30b5] to [5f7205ca].

     9      9                        racket/syntax
    10     10                        syntax/parse
    11     11                        pollen/setup))
    12     12   
    13     13   (require pollen/tag
    14     14            pollen/setup
    15     15            racket/function
           16  +         "cache.rkt"
    16     17            "tags-html.rkt"
    17     18            "snippets-html.rkt"
    18     19            "crystalize.rkt")
    19     20   
    20     21   (provide (all-defined-out)
    21         -         (all-from-out "crystalize.rkt" "snippets-html.rkt"))
           22  +         (all-from-out "crystalize.rkt" "snippets-html.rkt" "cache.rkt"))
    22     23   
    23     24   (module setup racket/base
    24     25     (require syntax/modresolve
    25     26              racket/runtime-path
    26     27              pollen/setup)
    27     28     (provide (all-defined-out))
    28     29     (define poly-targets '(html))
    29     30     (define block-tags (append '(title style dt note) default-block-tags))
    30     31   
    31     32     (define-runtime-path tags-html.rkt     "tags-html.rkt")
    32     33     (define-runtime-path snippets-html.rkt "snippets-html.rkt")
    33     34     (define-runtime-path dust.rkt          "dust.rkt")
    34     35     (define-runtime-path crystalize.rkt    "crystalize.rkt")
           36  +  (define-runtime-path cache.rkt         "cache.rkt")
    35     37     (define cache-watchlist
    36     38       (map resolve-module-path
    37     39            (list tags-html.rkt
    38     40                  snippets-html.rkt
    39     41                  dust.rkt
           42  +               cache.rkt
    40     43                  crystalize.rkt))))
    41     44   
    42     45   (case (current-poly-target)
    43     46     [(html) (init-cache-db!)])
    44     47   
    45     48   ;; Macro for defining tag functions that automatically branch based on the 
    46     49   ;; current output format and the list of poly-targets in the setup module.

Modified rss-feed.rkt from [ca1031f8] to [502871b7].

     8      8   (require txexpr
     9      9            racket/match
    10     10            racket/file
    11     11            racket/date
    12     12            racket/string
    13     13            db/base
    14     14            "dust.rkt"
    15         -         "crystalize.rkt")
           15  +         "cache.rkt")
    16     16   
    17     17   (provide main)
    18     18   
    19     19   (define feed-author default-authorname)
    20     20   (define feed-author-email "joel@jdueck.net")
    21     21   (define feed-title "The Local Yarn (Beta)")
    22     22   (define feed-site-url "https://thelocalyarn.com")