diff --git a/build/Site.hs b/build/Site.hs index e1ce356..fdc6320 100644 --- a/build/Site.hs +++ b/build/Site.hs @@ -2,8 +2,8 @@ {-# LANGUAGE OverloadedStrings #-} module Site (rules) where -import Control.Monad (filterM, forM, forM_, when) -import Data.Char (toUpper) +import Control.Monad (forM, forM_, when) +import Data.Char (isSpace, toUpper) import Data.List (groupBy, isPrefixOf, sort, sortBy) import Data.Map.Strict (Map) import Data.Maybe (catMaybes, fromMaybe, listToMaybe) @@ -55,6 +55,20 @@ homePortals = , ("Miscellany", "miscellany") ] +-- | Default number of cards shown per library shelf. The sidecar +-- 'featured:' list may push this up to 'libraryShelfMax'. +libraryShelfCap :: Int +libraryShelfCap = 4 + +-- | Hard ceiling on cards per shelf, regardless of sidecar length. +libraryShelfMax :: Int +libraryShelfMax = 5 + +-- | Optional prose intro lifted into @$library-intro$@ on the library +-- page. Matched but not routed; consumed via the @"body"@ snapshot. +libraryIntroId :: Identifier +libraryIntroId = fromFilePath "content/library.md" + -- Poems inside collection subdirectories, excluding their index pages. collectionPoems :: Pattern collectionPoems = "content/poetry/*/*.md" .&&. complement "content/poetry/*/index.md" @@ -257,7 +271,8 @@ rules = do match ("content/*.md" .&&. complement "content/index.md" .&&. complement "content/commonplace.md" - .&&. complement "content/colophon.md") $ do + .&&. complement "content/colophon.md" + .&&. complement "content/library.md") $ do route $ gsubRoute "content/" (const "") `composeRoutes` setExtension "html" compile $ pageCompiler @@ -457,6 +472,13 @@ rules = do >>= loadAndApplyTemplate "templates/default.html" ctx >>= relativizeUrls + -- --------------------------------------------------------------------------- + -- Library intro — optional prose block (typically a blockquote) lifted + -- into @$library-intro$@ at the top of /library.html. Matched but not + -- routed; the body snapshot is consumed by the library rule below. + -- --------------------------------------------------------------------------- + match "content/library.md" $ compile sidecarCompiler + -- --------------------------------------------------------------------------- -- Library — portal-grouped view over the /new.html dataset, deduplicated -- by primary portal. An item's primary portal is the top segment of the @@ -465,14 +487,27 @@ rules = do -- silently dropped from the library (they remain on /new.html and on any -- tag pages their frontmatter produces). -- + -- Each shelf is capped at 'libraryShelfCap' items by default. A portal's + -- tag-meta sidecar may carry a 'featured:' list of content-rooted paths + -- (e.g. @content/essays/foo.md@); featured items are placed first, in + -- listed order, and the remainder is filled by recency up to a hard + -- ceiling of 'libraryShelfMax'. Featured paths that don't resolve to an + -- item in the portal (wrong primary portal, or typo) are silently + -- dropped. When the unfiltered portal has more items than the shelf + -- shows, @$-has-more$@ is exposed so the template can render a + -- "More on this shelf →" affordance linking to the portal's tag page. + -- -- Each card uses the shared item-card partial, with cross-portal filings - -- rendered in the card's tag footer via 'tagLinksFieldExcludingScope', + -- rendered in the card's tag footer via 'tagLinksFieldExcludingTopSegment', -- scoped to the section's portal so the portal's own tag is suppressed. -- --------------------------------------------------------------------------- create ["library.html"] $ do route idRoute compile $ do - let knownPortals = map snd homePortals + sidecarIds <- getMatches ("content/tag-meta/*.md" + .||. "content/tag-meta/**/*.md") + let sidecarSet = Set.fromList sidecarIds + knownPortals = map snd homePortals -- Top segment of the first tag that names a known portal. -- Nothing when no tag matches — item is excluded from library. @@ -499,31 +534,80 @@ rules = do <> constField "full-abstract" "true" <> essayCtx - portalList name p = listField name (portalItemCtx p) $ do - essays <- loadAll (allEssays .&&. hasNoVersion) - posts <- loadAll ("content/blog/*.md" .&&. hasNoVersion) - fiction <- loadAll ("content/fiction/*.md" .&&. hasNoVersion) - poetry <- loadAll (allPoetry .&&. hasNoVersion) - music <- loadAll ("content/music/*/index.md" .&&. hasNoVersion) - let allItems = essays ++ posts ++ fiction ++ poetry ++ music - filtered <- filterM (\i -> (== Just p) <$> primaryPortalOf i) allItems - sorted <- recentFirstByDisplay filtered - -- noResult here makes the field absent, so the template's - -- $if(p-entries)$ gate evaluates false and the section is - -- omitted entirely (rather than rendering an empty