From 9711e4941fbfa433cb905da60dff2210f08af01d Mon Sep 17 00:00:00 2001 From: Dominik Schrempf Date: Fri, 28 Aug 2026 10:18:05 +0200 Subject: [PATCH 1/4] Factor out nonEmpty for list fields A listField over an empty list still counts as present, so `$if(...)$` is True and a template renders a section heading above no tiles. Both places that worked around this required the same guard. No change in output. --- site.hs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/site.hs b/site.hs index 99ffe664..ae92a2c8 100644 --- a/site.hs +++ b/site.hs @@ -527,8 +527,8 @@ whoWeAreCtx people = ofMetadataFieldCurrent :: Bool -> String -> String -> [Item String] -> Compiler [Item String] ofMetadataFieldCurrent cur field value items = do items' <- ofMetadataField field value items - current <- - filterM + nonEmpty + =<< filterM ( \item -> do mbTenureStart <- getMetadataField (itemIdentifier item) "tenureStart" mbTenureStop <- getMetadataField (itemIdentifier item) "tenureEnd" @@ -537,11 +537,6 @@ whoWeAreCtx people = Just date -> not cur ) items' - -- Fail (rather than return an empty list) when nobody matches, so the - -- corresponding listField is *absent* and `$if(...)$` in templates is - -- False. Otherwise an empty list still counts as "present". - guard (not (null current)) - pure current -- podcast --------------------------------------------------------------------------------------------- podcastListCtx :: [Item String] -> Context String @@ -601,6 +596,14 @@ reportCtx = dateField "date" "%B %d, %0Y" -- UTILS ----------------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------- +{- | Fail (rather than return an empty list) when nothing is left, so that the +'listField' built from the result is *absent* and `$if(...)$` in templates is +False. Otherwise an empty list still counts as "present" and a template renders +a section heading above no tiles at all. +-} +nonEmpty :: [a] -> Compiler [a] +nonEmpty items = guard (not (null items)) >> pure items + -- | filter list of item string based on whether or not the field exists filterMetadataField :: String -> [Item String] -> Compiler [Item String] filterMetadataField field = @@ -612,16 +615,14 @@ filterMetadataField field = -- | filter list of item string based on the given value to match on the given metadata field ofMetadataField :: String -> String -> [Item String] -> Compiler [Item String] -ofMetadataField field value items = do - matching <- - filterM +ofMetadataField field value items = + nonEmpty + =<< filterM ( \item -> do mbField <- getMetadataField (itemIdentifier item) field return $ Just value == mbField ) items - guard (not (null matching)) - pure matching -- | sort list of item based on the given metadata field sortFromMetadataField :: String -> [Item String] -> Compiler [Item String] From 11199c350aebb0f69aae5bb83a33484742315970 Mon Sep 17 00:00:00 2001 From: Dominik Schrempf Date: Fri, 28 Aug 2026 10:19:54 +0200 Subject: [PATCH 2/4] Derive event dates from front matter instead of a status field Events carried a prose `daterange` and a hand-maintained `status`, and the home page showed every event marked active. Nobody flips that field once an event is over, so five of seven events still claimed to be active and four of those had already happened. Consequence: the "front page tiles are weird choices" bias towards events (#632). Replace both with `starts:` and, for a multi-day event, `ends:` as ISO dates. `EventDates` parses them, `formatDateRange` words the range the templates print, and the home page keeps only events that have not finished yet. The events index sorts by start date rather than announcement date. The cutoff is the build date, so a finished event lingers until the next push to hakyll. --- events/2023-ghc-development-workshop.markdown | 4 +- .../2024-haskell-ecosystem-workshop.markdown | 4 +- .../2025-haskell-ecosystem-workshop.markdown | 3 +- ...025-haskell-implementors-workshop.markdown | 3 +- events/2026-amerihac.markdown | 4 +- .../2026-haskell-ecosystem-workshop.markdown | 3 +- ...026-haskell-implementors-workshop.markdown | 3 +- haskell-foundation.cabal | 1 + site.hs | 73 ++++++++++++++++--- 9 files changed, 73 insertions(+), 25 deletions(-) diff --git a/events/2023-ghc-development-workshop.markdown b/events/2023-ghc-development-workshop.markdown index 99dd52d2..23d06518 100644 --- a/events/2023-ghc-development-workshop.markdown +++ b/events/2023-ghc-development-workshop.markdown @@ -1,8 +1,8 @@ --- title: 2023 GHC Contributors' Workshop published: 2023-03-01 -daterange: June 7-9, 2023 -status: inactive +starts: 2023-06-07 +ends: 2023-06-09 location: Rapperswil, Switzerland summary: A hands-on introduction to working on GHC, colocated with Zurihac 2023 --- diff --git a/events/2024-haskell-ecosystem-workshop.markdown b/events/2024-haskell-ecosystem-workshop.markdown index 75df2429..7c9b3545 100644 --- a/events/2024-haskell-ecosystem-workshop.markdown +++ b/events/2024-haskell-ecosystem-workshop.markdown @@ -1,8 +1,8 @@ --- title: 2024 Haskell Ecosystem Workshop published: 2024-02-29 -daterange: June 6-7, 2024 -status: active +starts: 2024-06-06 +ends: 2024-06-07 location: Rapperswil, Switzerland summary: Building on-ramps for the Haskell Ecosystem and its Tools --- diff --git a/events/2025-haskell-ecosystem-workshop.markdown b/events/2025-haskell-ecosystem-workshop.markdown index e029d59d..298f3bd7 100644 --- a/events/2025-haskell-ecosystem-workshop.markdown +++ b/events/2025-haskell-ecosystem-workshop.markdown @@ -1,8 +1,7 @@ --- title: 2025 Haskell Ecosystem Workshop published: 2024-03-05 -daterange: June 5, 2025 -status: active +starts: 2025-06-05 location: Rapperswil, Switzerland summary: Lessons from and for Large Haskell Deployments --- diff --git a/events/2025-haskell-implementors-workshop.markdown b/events/2025-haskell-implementors-workshop.markdown index a36a071b..8e87cc93 100644 --- a/events/2025-haskell-implementors-workshop.markdown +++ b/events/2025-haskell-implementors-workshop.markdown @@ -1,8 +1,7 @@ --- title: 2025 Haskell Implementors' Workshop published: 2025-02-14 -daterange: June 6, 2025 -status: active +starts: 2025-06-06 location: Rapperswil, Switzerland summary: Forum for people interested in the design and development of Haskell implementations, tools, libraries, and supporting infrastructure. --- diff --git a/events/2026-amerihac.markdown b/events/2026-amerihac.markdown index e4b91ce1..74cfc4e3 100644 --- a/events/2026-amerihac.markdown +++ b/events/2026-amerihac.markdown @@ -1,8 +1,8 @@ --- title: 2026 Haskell Foundation North American Hackathon (AmeriHac) published: 2025-09-13 -daterange: Feb 7-8, 2026 -status: active +starts: 2026-02-07 +ends: 2026-02-08 location: New York, New York summary: Open Source Haskell hackathon --- diff --git a/events/2026-haskell-ecosystem-workshop.markdown b/events/2026-haskell-ecosystem-workshop.markdown index 7641a181..d9d19227 100644 --- a/events/2026-haskell-ecosystem-workshop.markdown +++ b/events/2026-haskell-ecosystem-workshop.markdown @@ -1,8 +1,7 @@ --- title: 2026 Haskell Ecosystem Workshop published: 2026-02-13 -daterange: June 4, 2026 -status: active +starts: 2026-06-04 location: Rapperswil, Switzerland summary: Lessons from and for Large Haskell Deployments --- diff --git a/events/2026-haskell-implementors-workshop.markdown b/events/2026-haskell-implementors-workshop.markdown index b37440cf..2f1aa5b1 100644 --- a/events/2026-haskell-implementors-workshop.markdown +++ b/events/2026-haskell-implementors-workshop.markdown @@ -1,8 +1,7 @@ --- title: 2026 Haskell Implementors' Workshop published: 2026-02-13 -daterange: June 5, 2026 -status: active +starts: 2026-06-05 location: Rapperswil, Switzerland summary: Forum for people interested in the design and development of Haskell implementations, tools, libraries, and supporting infrastructure. --- diff --git a/haskell-foundation.cabal b/haskell-foundation.cabal index 7e5689c3..3004ffdb 100644 --- a/haskell-foundation.cabal +++ b/haskell-foundation.cabal @@ -11,6 +11,7 @@ executable site , pandoc >=2.11 && <3.8 , filepath ^>= 1.5 , text + , time ghc-options: -threaded default-language: Haskell2010 diff --git a/site.hs b/site.hs index ae92a2c8..1e56f75f 100644 --- a/site.hs +++ b/site.hs @@ -7,7 +7,11 @@ import Control.Monad (filterM, guard) import Control.Monad.ListM (sortByM) import Data.List (sortOn, isPrefixOf) import Data.Maybe (fromMaybe, isJust) +import Data.Ord (Down (..)) import qualified Data.Text as T +import Data.Time.Calendar (Day, toGregorian) +import Data.Time.Clock (getCurrentTime, utctDay) +import Data.Time.Format (defaultTimeLocale, formatTime, parseTimeM) import Hakyll import Hakyll.Web.Html.RelativizeUrls (relativizeUrls) import Hakyll.Web.Template (loadAndApplyTemplate) @@ -268,7 +272,7 @@ main = hakyllWith config $ do route idRoute compile $ do sponsors <- buildBoilerplateCtx (Just "Events") - ctx <- allEventsCtx <$> (recentFirst =<< loadAll ("events/*.markdown" .&&. hasNoVersion)) + ctx <- allEventsCtx <$> loadAll ("events/*.markdown" .&&. hasNoVersion) makeItem "" >>= loadAndApplyTemplate "templates/events/list.html" ctx @@ -277,14 +281,11 @@ main = hakyllWith config $ do match "events/*.markdown" $ do route $ setExtension "html" - let ctxt = - mconcat - [defaultContext] compile $ do sponsors <- buildBoilerplateCtx Nothing pandocCompiler >>= applyAsTemplate sponsors - >>= loadAndApplyTemplate "templates/events/page.html" ctxt + >>= loadAndApplyTemplate "templates/events/page.html" eventCtx >>= loadAndApplyTemplate "templates/boilerplate.html" sponsors >>= relativizeUrls @@ -366,7 +367,7 @@ main = hakyllWith config $ do careersCtx <- careersCtx . reverse <$> loadAll ("careers/*.markdown" .&&. hasNoVersion) announces <- take 1 <$> (recentFirst =<< loadAll @String ("news/*/**.markdown" .&&. hasNoVersion)) let announceCtx = announcementsCtx announces - eventsCtx <- activeEventsCtx <$> (recentFirst =<< loadAll ("events/*.markdown" .&&. hasNoVersion)) + eventsCtx <- upcomingEventsCtx <$> loadAll ("events/*.markdown" .&&. hasNoVersion) makeItem "" >>= loadAndApplyTemplate "templates/homepage.html" (podcastsCtx <> careersCtx <> announceCtx <> eventsCtx) @@ -565,17 +566,63 @@ announcementsCtx :: [Item String] -> Context String announcementsCtx ads = listField "announcements" newsItemCtx (pure ads) --- Events +-- Events ----------------------------------------------------------------------------------------------- +-- | All events, the ones happening soonest last, as an archive reads. allEventsCtx :: [Item String] -> Context String allEventsCtx evts = - listField "events" defaultContext (pure evts) + listField "events" eventCtx (sortOnM (fmap (Down . eventStarts) . eventDates) evts) <> defaultContext -activeEventsCtx :: [Item String] -> Context String -activeEventsCtx evts = - listField "events" defaultContext (ofMetadataField "status" "active" evts) +-- | Only the events that have not finished yet, the ones happening soonest first. +upcomingEventsCtx :: [Item String] -> Context String +upcomingEventsCtx evts = + listField "events" eventCtx (nonEmpty =<< upcomingEvents evts) <> defaultContext + where + upcomingEvents es = do + today <- utctDay <$> unsafeCompiler getCurrentTime + sortOnM (fmap eventStarts . eventDates) =<< filterM (fmap ((>= today) . eventEnds) . eventDates) es + +-- | A single event, with the date range the templates show derived from 'EventDates'. +eventCtx :: Context String +eventCtx = + field "daterange" (fmap formatDateRange . eventDates) + <> defaultContext + +{- | The span of days an event covers, from its @starts@ and (for a multi-day +event) @ends@ front matter fields. Keeping the dates as data rather than as the +prose field they replace lets the home page drop past events by itself, and +leaves one place where a range is worded. +-} +data EventDates = EventDates {eventStarts :: Day, eventEnds :: Day} + +eventDates :: Item a -> Compiler EventDates +eventDates item = do + starts <- isoDay "starts" =<< getMetadataField' ident "starts" + ends <- traverse (isoDay "ends") =<< getMetadataField ident "ends" + pure $ EventDates starts (fromMaybe starts ends) + where + ident = itemIdentifier item + isoDay name raw = + maybe (fail $ show ident <> ": " <> name <> " is not a YYYY-MM-DD date: " <> raw) pure $ + parseTimeM True defaultTimeLocale "%Y-%m-%d" raw + +{- | Word a span the way a person would: @June 4, 2026@ for a single day, +@June 7-9, 2023@ within one month, @May 30 - June 2, 2026@ within one year, and +both dates in full across years. +-} +formatDateRange :: EventDates -> String +formatDateRange (EventDates starts ends) + | starts == ends = fmt "%B %-d, %Y" starts + | (startYear, startMonth) == (endYear, endMonth) = fmt "%B %-d" starts <> "-" <> fmt "%-d, %Y" ends + | startYear == endYear = fmt "%B %-d" starts <> " - " <> fmt "%B %-d, %Y" ends + | otherwise = fmt "%B %-d, %Y" starts <> " - " <> fmt "%B %-d, %Y" ends + where + (startYear, startMonth, _) = toGregorian starts + (endYear, endMonth, _) = toGregorian ends + fmt :: String -> Day -> String + fmt = formatTime defaultTimeLocale partnershipCtx :: [Item String] -> Context String partnershipCtx evts = @@ -604,6 +651,10 @@ a section heading above no tiles at all. nonEmpty :: [a] -> Compiler [a] nonEmpty items = guard (not (null items)) >> pure items +-- | Sort by a key that can only be computed in the 'Compiler' monad. +sortOnM :: (Ord b) => (a -> Compiler b) -> [a] -> Compiler [a] +sortOnM key items = map fst . sortOn snd <$> traverse (\x -> (,) x <$> key x) items + -- | filter list of item string based on whether or not the field exists filterMetadataField :: String -> [Item String] -> Compiler [Item String] filterMetadataField field = From e2d3ee59507c322cf775945aebfd0ec8194ee44e Mon Sep 17 00:00:00 2001 From: Dominik Schrempf Date: Fri, 28 Aug 2026 10:21:43 +0200 Subject: [PATCH 3/4] Give the home page curated pins and separate bands The front page put open job ad, one news entry, one podcast episode and every active event into a single grid, so events crowded out everything else and there was no way to put anything specific on top. See issues "manually curated links at top" (#624) and "the front page tiles are weird choices":(#632). Split the grid into four bands, each hidden when it has nothing to show: the curated pins, the latest news, the events still to come, and the latest podcast episode. News goes from one entry to two, and the news and events bands link on to their index pages. Pins come from featured.markdown, an ordered list of paths of pages that exist elsewhere in the site. A pin is a pointer, never a second copy of the text. The tile shows the page's title, its summary (or its first paragraph) and a link to it. That is, news entries, job openings and events can all be pinned. A path naming no page fails the build. Anything pinned is dropped from the automatic bands below. This replaces the hard-wired band of open job ads: the front page is curated now, and a new opening is pinned by hand. --- assets/css/tailwind.built.css | 16 ++++++++ featured.markdown | 16 ++++++++ site.hs | 67 +++++++++++++++++++++++++----- templates/featured/tile.html | 11 +++++ templates/homepage.html | 77 +++++++++++++++++++++-------------- 5 files changed, 147 insertions(+), 40 deletions(-) create mode 100644 featured.markdown create mode 100644 templates/featured/tile.html diff --git a/assets/css/tailwind.built.css b/assets/css/tailwind.built.css index c6baab9b..8bb4f580 100644 --- a/assets/css/tailwind.built.css +++ b/assets/css/tailwind.built.css @@ -969,6 +969,13 @@ margin-block-end: calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse))); } } + .space-y-16 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 16) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 16) * calc(1 - var(--tw-space-y-reverse))); + } + } .gap-x-3 { -moz-column-gap: calc(var(--spacing) * 3); column-gap: calc(var(--spacing) * 3); @@ -1719,6 +1726,15 @@ gap: calc(var(--spacing) * 8); } } + .md\:space-y-24 { + @media (width >= 48rem) { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 24) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 24) * calc(1 - var(--tw-space-y-reverse))); + } + } + } .md\:px-6 { @media (width >= 48rem) { padding-inline: calc(var(--spacing) * 6); diff --git a/featured.markdown b/featured.markdown new file mode 100644 index 00000000..71adde49 --- /dev/null +++ b/featured.markdown @@ -0,0 +1,16 @@ +--- +# Tiles pinned to the top of the home page, in this order. +# +# Every entry is the path of a page that already exists somewhere in the site — +# a job opening, a news entry, an event, ... — so a pinned tile is a pointer, +# never a second copy of the text. The tile shows the page's title, its +# `summary` (or its first paragraph if it has none) and a link to it. Pin the +# page a reader is meant to end up on: a job opening belongs here rather than in +# a news entry pointing at it, so that applying is one click away, not two. +# +# To unpin something, delete its line. An empty list drops the whole band; a +# path that names no page fails the build. +featured: + - careers/head-of-development.markdown + - news/2026-08-05/technical-leadership-committee.markdown +--- diff --git a/site.hs b/site.hs index 1e56f75f..e2ae7356 100644 --- a/site.hs +++ b/site.hs @@ -55,6 +55,19 @@ tailwindEntryPoint, tailwindBuilt :: Pattern tailwindEntryPoint = "assets/css/tailwind.css" tailwindBuilt = "assets/css/tailwind.built.css" +{- | The curation file behind the pinned tiles at the top of the home page. It +holds no content of its own, only an ordered list of paths of pages elsewhere in +the site, so a pinned tile still has a single source of truth. It is compiled +(to pick up edits) but never routed. +-} +featuredCuration :: Identifier +featuredCuration = "featured.markdown" + +-- | How many news entries the home page shows below the pinned tiles. Even, so +-- that the two-column grid comes out square. +homepageNewsCount :: Int +homepageNewsCount = 2 + -------------------------------------------------------------------------------------------------------- -- MAIN GENERATION ------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------- @@ -359,18 +372,24 @@ main = hakyllWith config $ do match "**/*.markdown" $ version "description" $ compile pandocPlainCompiler -- home page ------------------------------------------------------------------------------------------- + -- + -- Four bands of tiles: the curated pins from 'featuredCuration' first, then + -- the latest news, the events still to come, and the latest podcast + -- episode. Anything pinned is dropped from the automatic bands so it is not + -- shown twice. + match (fromList [featuredCuration]) $ compile getResourceBody + create ["index.html"] $ do route idRoute compile $ do sponsors <- buildBoilerplateCtx (Just "Haskell Foundation") + featured <- loadFeatured podcastsCtx <- podcastListCtx . take 1 . reverse . sortOn podcastOrd <$> loadAll ("podcast/*/index.markdown" .&&. hasVersion "raw") - careersCtx <- careersCtx . reverse <$> loadAll ("careers/*.markdown" .&&. hasNoVersion) - announces <- take 1 <$> (recentFirst =<< loadAll @String ("news/*/**.markdown" .&&. hasNoVersion)) - let announceCtx = announcementsCtx announces - eventsCtx <- upcomingEventsCtx <$> loadAll ("events/*.markdown" .&&. hasNoVersion) + announces <- take homepageNewsCount . excluding featured <$> (recentFirst =<< loadAll @String ("news/*/**.markdown" .&&. hasNoVersion)) + events <- excluding featured <$> loadAll ("events/*.markdown" .&&. hasNoVersion) makeItem "" - >>= loadAndApplyTemplate "templates/homepage.html" (podcastsCtx <> careersCtx <> announceCtx <> eventsCtx) + >>= loadAndApplyTemplate "templates/homepage.html" (podcastsCtx <> featuredCtx featured <> announcementsCtx announces <> upcomingEventsCtx events) >>= loadAndApplyTemplate "templates/boilerplate.html" sponsors >>= relativizeUrls @@ -500,15 +519,19 @@ slugField name = -- news ------------------------------------------------------------------------------------------------ -- | Context for a single news entry, shared by the index tiles, the homepage --- announcement and the standalone article page. `teaser` is the entry's first --- paragraph as plain text (from the "description" version), used as a preview --- so the index links to the full article rather than inlining it. +-- announcement and the standalone article page. newsItemCtx :: Context String newsItemCtx = - field "teaser" (loadBody . setVersion (Just "description") . itemIdentifier) + teaserFromDescription <> dateField "date" "%B %e, %Y" <> defaultContext +-- | The item's first paragraph as plain text (from the "description" version), +-- used as a tile preview so the tile links to the full page rather than +-- inlining it. +teaserFromDescription :: Context String +teaserFromDescription = field "teaser" (loadBody . setVersion (Just "description") . itemIdentifier) + -- faq ------------------------------------------------------------------------------------------------- faqCtx :: [Item String] -> Context String faqCtx entries = @@ -564,7 +587,27 @@ hiringSponsorsCtx sponsors = announcementsCtx :: [Item String] -> Context String announcementsCtx ads = - listField "announcements" newsItemCtx (pure ads) + listField "announcements" newsItemCtx (nonEmpty ads) + +-- Featured --------------------------------------------------------------------------------------------- + +{- | The pages pinned to the top of the home page, in the order 'featuredCuration' +lists them. A path that does not name a compiled page fails the build, so a +renamed or deleted page cannot silently leave a hole on the front page. +-} +loadFeatured :: Compiler [Item String] +loadFeatured = do + paths <- fromMaybe [] . lookupStringList "featured" <$> getMetadata featuredCuration + traverse (load . fromFilePath) paths + +{- | A pinned tile. Pins point at pages from any section, so the tile only uses +what every section has: a title, a blurb (the page's own @summary@ where it has +one, otherwise its first paragraph) and a link (the external @link@ of a +link-out news entry, otherwise the page's own URL). +-} +featuredCtx :: [Item String] -> Context String +featuredCtx items = + listField "featured" (teaserFromDescription <> defaultContext) (nonEmpty items) -- Events ----------------------------------------------------------------------------------------------- @@ -651,6 +694,10 @@ a section heading above no tiles at all. nonEmpty :: [a] -> Compiler [a] nonEmpty items = guard (not (null items)) >> pure items +-- | Drop the items already shown elsewhere on the same page. +excluding :: [Item a] -> [Item b] -> [Item b] +excluding shown = filter ((`notElem` map itemIdentifier shown) . itemIdentifier) + -- | Sort by a key that can only be computed in the 'Compiler' monad. sortOnM :: (Ord b) => (a -> Compiler b) -> [a] -> Compiler [a] sortOnM key items = map fst . sortOn snd <$> traverse (\x -> (,) x <$> key x) items diff --git a/templates/featured/tile.html b/templates/featured/tile.html new file mode 100644 index 00000000..0b8e4dd4 --- /dev/null +++ b/templates/featured/tile.html @@ -0,0 +1,11 @@ +
+
+
+

$title$

+

$if(summary)$$summary$$else$$teaser$$endif$

+ +
+
+
diff --git a/templates/homepage.html b/templates/homepage.html index ae2cf845..1ec56a41 100644 --- a/templates/homepage.html +++ b/templates/homepage.html @@ -15,42 +15,59 @@

Amplify Haskell’s impact on humanity. -
-
- $if(openreqs)$ - $for(openreqs)$ - $partial("templates/careers/tile.html")$ - $endfor$ - $endif$ +
- $if(announcements)$ - $for(announcements)$ - $partial("templates/news/frontpage.html")$ + $if(featured)$ +
+ $for(featured)$ + $partial("templates/featured/tile.html")$ $endfor$ - $endif$ - $for(episodes)$ -
-
-

- $episode$ – $title$ -

-
- $body$ - +
+ $endif$ + + $if(announcements)$ +
+

Latest news

+
+ $for(announcements)$ + $partial("templates/news/frontpage.html")$ + $endfor$ +
+

>> All news

+
+ $endif$ + + $if(events)$ +
+

Upcoming events

+
+ $for(events)$ + $partial("templates/events/tile.html")$ + $endfor$ +
+

>> All events

+
+ $endif$ + + $for(episodes)$ +
+

Latest podcast episode

+
+
+

+ $episode$ – $title$ +

+
+ $body$ +
- $endfor$ - - $if(events)$ - $for(events)$ - $partial("templates/events/tile.html")$ - $endfor$ - $endif$ +
+
+ $endfor$ -
From b650035a226ed8295e29decdb6b3ffa5f8a7da23 Mon Sep 17 00:00:00 2001 From: Dominik Schrempf Date: Fri, 28 Aug 2026 10:23:29 +0200 Subject: [PATCH 4/4] Retire the Head of Development announcement Its whole body was a pointer to the job ad, which the home page now pins directly, so the entry only stood between a reader and the thing they came for. Redirect its URL to the ad rather than dropping it: the announcement has been linked to from elsewhere. `retiredPages` feeds Hakyll's `createRedirects`, which writes a meta-refresh and rel=canonical stub at the old path. --- news/2026-08-10/head-of-development.markdown | 7 ------- site.hs | 13 +++++++++++++ 2 files changed, 13 insertions(+), 7 deletions(-) delete mode 100644 news/2026-08-10/head-of-development.markdown diff --git a/news/2026-08-10/head-of-development.markdown b/news/2026-08-10/head-of-development.markdown deleted file mode 100644 index 720ddd4f..00000000 --- a/news/2026-08-10/head-of-development.markdown +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: The Haskell Foundation Seeks a Head of Development ---- - -The Haskell Foundation is looking to hire a Head of Development: an exceptional fundraiser to help it bring its operations to the next level. The Head of Development will own the Foundation's fundraising process, growing and retaining corporate sponsors and diversifying the Foundation's funding sources. - -The role is open to applicants from any country, and the Foundation is open to part-time applicants. See the [full job description](/careers/head-of-development.html) for details on the role and how to apply. diff --git a/site.hs b/site.hs index e2ae7356..c9166827 100644 --- a/site.hs +++ b/site.hs @@ -68,6 +68,17 @@ featuredCuration = "featured.markdown" homepageNewsCount :: Int homepageNewsCount = 2 +{- | Pages that have been retired, and where a reader should end up instead. +'createRedirects' turns each into a meta-refresh stub at the old URL, so links +already out in the wild keep working. Add an entry here rather than leaving a +page in place purely so that its URL resolves. +-} +retiredPages :: [(Identifier, String)] +retiredPages = + -- Was an announcement whose only content was a pointer to the job ad; the + -- ad is now pinned on the home page directly (see 'featuredCuration'). + [("news/2026-08-10/head-of-development.html", "/careers/head-of-development.html")] + -------------------------------------------------------------------------------------------------------- -- MAIN GENERATION ------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------------------------- @@ -159,6 +170,8 @@ main = hakyllWith config $ do >>= relativizeUrls matchMetadata "news/**.markdown" hasLink $ compile pandocCompiler + createRedirects retiredPages + categories <- buildCategories "news/**.markdown" (fromCapture "news/categories/**.html") tagsRules categories $ \category catId -> compile $ do