From d65b4e88cf1a6625f368a6664565c2cfa19a770c Mon Sep 17 00:00:00 2001 From: Ivan Kocienski Date: Mon, 19 Jan 2026 09:05:07 +0000 Subject: [PATCH 1/8] The blog Basic blog functionality where posts get compiled (before deploy) and are then rendered with an index and show page. Also implements "Not Found" and has a refactored meta-tags pipeline. --- README.md | 24 +++ blog-posts/blog-introduction.md | 22 ++ blog-posts/escaping-big-tech.md | 45 ++++ elm.json | 2 +- package.json | 5 +- scripts/convert-blog-posts-to-json-payload.js | 204 ++++++++++++++++++ src/Copy/CaseStudy.elm | 48 +---- src/Copy/Keys.elm | 21 +- src/Copy/Text.elm | 54 ++++- src/Main.elm | 177 ++++++++++++--- src/MetaTags.elm | 122 ++++++----- src/Model.elm | 75 ++++++- src/Page/Blog.elm | 166 ++++++++++++++ src/Page/Index.elm | 64 ++++-- src/Page/NotFound.elm | 46 ++++ src/Route.elm | 14 ++ src/assets/blog-data.json | 1 + src/assets/main.js | 6 +- 18 files changed, 939 insertions(+), 157 deletions(-) create mode 100644 blog-posts/blog-introduction.md create mode 100644 blog-posts/escaping-big-tech.md create mode 100644 scripts/convert-blog-posts-to-json-payload.js create mode 100644 src/Page/Blog.elm create mode 100644 src/Page/NotFound.elm create mode 100644 src/assets/blog-data.json diff --git a/README.md b/README.md index 0dc5a44..f130582 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,30 @@ We're using [elm-test-rs](https://github.com/mpizenberg/elm-test-rs) to run [elm - About Us profile text is in `Copy.AboutUs` - Page templates for each `Route` are defined in e.g. `Page.Index` and `Page.CaseStudy` +### Writing blog posts + +All blog posts are just markdown text files in `/blog-posts`. When you have your content +written just copy an existing post file and replace that content/meta data as needed. + +Then run `npm run build_blog`. This will validate that all post files have the correct +meta fields and output a `blog-data.json` payload. You can then reload your local server +and the new post should appear at the front of the list. + +Any added posts and changes to the `blog-data.json` file should be pushed to main branch +to make live on the public site. + +The meta fields are (currently): +- `author` Who wrote it. Can be any string. +- `title` The title of the post. Don't put the title in the content. Also appears in the + browser title field. +- `publish_date` Must be in YYYY-MM-DD format. +- `teaser` Short paragraph giving a brief overview of the main subject. Also used in the + page meta field for SEO. +- `keywords` A list of comma seperated key-words. Also used in the + page meta field for SEO. + +At this point there is no established way of embedding images. + ### Styling & layouts - TBC diff --git a/blog-posts/blog-introduction.md b/blog-posts/blog-introduction.md new file mode 100644 index 0000000..c099703 --- /dev/null +++ b/blog-posts/blog-introduction.md @@ -0,0 +1,22 @@ +--- +slug: 'introducing-the-cookie-blog' +author: 'Ivan Kocienski' +title: 'Introducing the Cookie Blog' +publish_date: '2026-01-14' +teaser: 'A little hello from us with an introduction to our plans for the coming year.' +keywords: 'introduction, thoughts, plan' +--- + +Hello! + +And welcome to the cookiewolf blog- our little slice of the internet where we can post ideas and updates about all the things we are getting up to. (Or that is the plan!) + +We are an active group in our local communities so it will be nice to take some of those experiences and put them into words. + +We also hope this will be where we can share resources with the wider community. + +Or maybe one week we just want to share our cookie recipes. + +With love, + +The Cookie Wolf humans. diff --git a/blog-posts/escaping-big-tech.md b/blog-posts/escaping-big-tech.md new file mode 100644 index 0000000..eab9623 --- /dev/null +++ b/blog-posts/escaping-big-tech.md @@ -0,0 +1,45 @@ +--- +slug: 'escaping-big-tech' +author: 'Ivan Kocienski' +title: 'Escaping Big Tech' +publish_date: '2026-01-01' +teaser: 'BigTech: over-dependence on centralised platforms' +keywords: 'introduction, thoughts, plan' +--- + +## Introduction + +Apple, Microsoft, Meta, Google, etc. are increasing their grip on more of our lives, both professional and personal. The problem is they're very convenient and have well established integration in our day-to-day activities. + +I think worrying about such things can seem unecassery and as long as our rights (and their legal obligations) are followed then there is nothing to worry about? + +But it doesn't take a lot of looking around to find examples of Big Tech abusing its status as a defacto monopoly and how they only seem happy to protect our rights when it aligns with their profit margins and quarterly performance reviews. + +## You are not alone + +[RABT] + +## So what can we do? + +Time to take our data back into our own hands. Maybe not overnight, but just a little, here and there. + +## Our tech stack + +At Cookiewolf we try to avoid as much of this as possible. Here are a few bits of tech we use +- [ghandi]() DNS +- [Hetzner]() VPS hosting +- [Netlify]() hosting +- [cloudron]() platform +- [whereby]() video conferencing +- [zulip]() chat platform +- [linux]() OS + +## We can do better + +Unfortunately a big part of our day-to-day software engineering efforts take place on Microsofts Github. We have all our repositories there and in turn all our bug tracking. Moving will not be easy. + +We hope to move to [gitlab] (either self hosted or as a paid PaaS) in 2026. + +## Conclusion + +I hope \ No newline at end of file diff --git a/elm.json b/elm.json index 8f9dc6d..1367f99 100644 --- a/elm.json +++ b/elm.json @@ -8,6 +8,7 @@ "direct": { "elm/browser": "1.0.2", "elm/core": "1.0.5", + "elm/json": "1.1.3", "elm/url": "1.0.0", "elm-explorations/markdown": "1.0.0", "hmsk/elm-vite-plugin-helper": "1.0.1", @@ -15,7 +16,6 @@ }, "indirect": { "elm/html": "1.0.0", - "elm/json": "1.1.3", "elm/time": "1.0.0", "elm/virtual-dom": "1.0.3", "robinheghan/murmur3": "1.0.0", diff --git a/package.json b/package.json index 982f28f..02791cc 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "serve": "vite preview", "format": "elm-format src/ --yes", "lint": "elm-review", - "test": "elm-test-rs" + "test": "elm-test-rs", + "build_blog": "node scripts/convert-blog-posts-to-json-payload.js blog-posts/ src/assets/blog-data.json" }, "devDependencies": { "elm-review": "^2.13.2", @@ -21,4 +22,4 @@ "@fontsource/poppins": "^5.2.6", "elm-debug-transformer": "^1.2.1" } -} +} \ No newline at end of file diff --git a/scripts/convert-blog-posts-to-json-payload.js b/scripts/convert-blog-posts-to-json-payload.js new file mode 100644 index 0000000..6cae5ad --- /dev/null +++ b/scripts/convert-blog-posts-to-json-payload.js @@ -0,0 +1,204 @@ +/* + +Don't run this script directly, use `npm run build_blog`! + +scripts/convert-blog-posts-to-json-payload.js + +Looks for markdown files in src directory and bundles all that data +into one json file for deployment. +*/ + +const fs = require('node:fs/promises'); +const process = require('process'); + +const propPattern = /^([^:]+):\s+'([^']+)'/; + +function isMarkdownFile(filename) { + return filename.endsWith(".md"); +} + +function readSourcePostFile(basePath, filename) { + return new Promise((resolve, reject) => { + + fs.readFile(basePath + "/" + filename, 'utf8') + .catch(reject) + .then(data => { + + let props = {}; + const lines = data.split("\n"); + let pos = 0; + + if (lines[pos] === "---") { + pos++; + + while (lines[pos] !== "---" && pos < lines.length) { + const found = propPattern.exec(lines[pos]); + if (found) { + const key = found[1]; + const value = found[2]; + props[key] = value; + } + + pos++; + } + if (lines[pos] === "---") { + pos++; + } + } + const payload = { + srcFile: filename, + props: props, + lines: lines.slice(pos) + }; + resolve(payload); + }); + }); +} + +const datePattern = /^(\d{4})-(\d{2})-(\d{2})$/; + +function normalizePostData(rawPostData) { + return new Promise((resolve, reject) => { + + let postBySlug = {}; + + const postData = + rawPostData + .map(rawPost => { + + const lookup = name => (rawPost.props[name] || '').trim(); + const badEnd = fieldName => reject("Bad/missing `" + fieldName + "` in `" + rawPost.srcFile + "`"); + + // FIXME? reject posts with no content? + + const title = lookup('title'); + if (title.length == 0) { badEnd('title'); } + + const author = lookup('author'); + if (author.length == 0) { badEnd('author'); } + + const teaser = lookup('teaser'); + if (teaser.length == 0) { badEnd('teaser'); } + + const keywords = lookup('keywords'); + if (keywords.length == 0) { badEnd('keywords'); } + + const rawPublishDateValue = lookup('publish_date'); + // if (rawPublishDateValue.length == 0) { badEnd('publish_date'); } + + const dateMatch = datePattern.exec(rawPublishDateValue); + let publishDate; + if (dateMatch) { + const year = parseInt(dateMatch[1]); + const month = parseInt(dateMatch[2]); + const day = parseInt(dateMatch[3]); + + publishDate = new Date(year, month, day); + + } else { + badEnd('publish_date'); + } + + const slug = + title + .toLowerCase() + .replaceAll(/\W+/g, '-') + .replace(/-*$/, ''); + + if (slug.length == 0) { + reject("Title produces empty slug in " + rawPost.srcFile); + return; + } + + if (postBySlug[slug]) { + reject("Slug is not unique in " + rawPost.srcFile); + return; + + } else { + postBySlug[slug] = true; + } + + const post = { + title: title, + author: author, + teaser: teaser, + keywords: keywords, + slug: slug, + publish_date: publishDate.toISOString().slice(0, 10), + content: rawPost['lines'].join("\n") + }; + return post; + }) + + postData.sort((a, b) => { + // by date (reversed) + if (a.publish_date < b.publish_date) return 1; + if (a.publish_date > b.publish_date) return -1; + + // by title + if (a.title < b.title) return -1; + if (a.title > b.title) return 1; + + // (should never be this) + return 0; + }); + + resolve(postData); + }); +} + +function savePostData(postData, targetFile) { + return fs.writeFile(targetFile, JSON.stringify(postData)); + + /* + return new Promise((resolve, reject) => { + fs + .writeFile(targetFile, JSON.stringify(postData)) + .then(resolve) + .catch(reject); + }); + */ +} + +function main() { + if (process.argv.length < 4) { + console.error("Missing src / output arguments"); + process.exit(-1); + } + + const srcDir = process.argv[2]; + const targetFile = process.argv[3]; + + console.log('convert-blog-posts-to-json-payload'); + console.log(" srcDir=" + srcDir); + console.log(" targetFile=" + targetFile) + + fs.readdir(srcDir) + .then(files => { + const postFileData = + files + .filter(isMarkdownFile) + .map(file => readSourcePostFile(srcDir, file)); + + if (postFileData.length == 0) { + console.error("No post files found?"); + return; + } + + Promise + .all(postFileData) + .then(normalizePostData) + .then(data => savePostData(data, targetFile)) + .then(() => { + console.log("all done."); + }) + .catch(problem => { + console.error("error:", problem); + process.exit(-1); + }); + + }); +} + +main(); + diff --git a/src/Copy/CaseStudy.elm b/src/Copy/CaseStudy.elm index d7b2a41..2c1ceff 100644 --- a/src/Copy/CaseStudy.elm +++ b/src/Copy/CaseStudy.elm @@ -1,40 +1,17 @@ -module Copy.CaseStudy exposing (CaseStudyKey(..), caseStudyFromId, caseStudyIdFromSlug) +module Copy.CaseStudy exposing (caseStudyFromSlug, codeReadingClub, foyer, newProjectInvite) import Model import Route -type CaseStudyKey - = CodeReadingClub - | Foyer - | NewProjectInvite - | FourZeroFour - - -caseStudyIdFromSlug : String -> CaseStudyKey -caseStudyIdFromSlug slug = +caseStudyFromSlug : String -> Maybe Model.CaseStudy +caseStudyFromSlug slug = case slug of "foyer" -> - Foyer + Just foyer _ -> - FourZeroFour - - -caseStudyFromId : CaseStudyKey -> Model.CaseStudy -caseStudyFromId id = - case id of - CodeReadingClub -> - codeReadingClub - - Foyer -> - foyer - - NewProjectInvite -> - newProjectInvite - - FourZeroFour -> - fourZeroFour + Nothing codeReadingClub : Model.CaseStudy @@ -125,18 +102,3 @@ newProjectInvite = , metaUrl = Nothing , metaImageSrc = Nothing } - - -fourZeroFour : Model.CaseStudy -fourZeroFour = - { name = "Project not found" - , title = "Project not found" - , teaserBackgroundSrc = "" - , teaserSummary = "" - , teaserLinkText = "" - , teaserHref = "mailto:hello@cookiewolf.coop" - , maybePageContent = Nothing - , metaTitle = "" - , metaUrl = Nothing - , metaImageSrc = Nothing - } diff --git a/src/Copy/Keys.elm b/src/Copy/Keys.elm index 8f88f52..1f40944 100644 --- a/src/Copy/Keys.elm +++ b/src/Copy/Keys.elm @@ -15,6 +15,8 @@ type Key -- Header = SiteTitle + | HomeTitle + | WindowTitle String | Strapline | Category ContentType | HomeMetaDescription @@ -24,16 +26,33 @@ type | WhoWeAreHeading | WhoWeAreMarkdown1 | WhoWeAreMarkdown2 + -- Case study + | CaseStudyTitle + | CaseStudySlug -- About Us | AboutUsSlug | AboutUsTitle | AboutUsMetaDescription | AboutUsSection Section | AboutUsProfileProjectsLabel - | CaseStudySlug | WhatWeDidHeading | ResultsHeading -- Footer | ContactUsHeading | ContactUsMarkdown | CompanyInformation + -- blog bits + | BlogSlug + | BlogNotFoundThing + | BlogHomeTitle + | BlogHomeReadMoreLink + | BlogIndexTitle + | BlogMetaDescription + | BlogCardReadMoreLink + | BlogByLineBy + | BlogByLineOn + -- "not found" bits + | NotFoundTitle + | NotFoundThing + | NotFoundDescription1 String + | NotFoundDescription2 diff --git a/src/Copy/Text.elm b/src/Copy/Text.elm index 2d50cc5..9ef6556 100644 --- a/src/Copy/Text.elm +++ b/src/Copy/Text.elm @@ -13,6 +13,12 @@ t key = SiteTitle -> "Cookiewolf" + HomeTitle -> + "Home" + + WindowTitle pageTitle -> + pageTitle ++ " - Cookiewolf" + Strapline -> "Building useful digital tools with you" @@ -53,6 +59,12 @@ Our members’ award-winning work spans projects with clients across the arts, t [Meet the people behind Cookiewolf](/about-us) """ + CaseStudyTitle -> + "Case study" + + CaseStudySlug -> + "case-study" + ThingsWeWorkOnHeading -> "Things We're Working On" @@ -79,9 +91,6 @@ Our members’ award-winning work spans projects with clients across the arts, t AboutUsProfileProjectsLabel -> "**Selected projects**: " - CaseStudySlug -> - "case-study" - WhatWeDidHeading -> "What We Did" @@ -100,3 +109,42 @@ Got a project? Questions? Just want a chat? Email us at: CompanyInformation -> "Cookiewolf Co-op Ltd is registered in England & Wales (No. 13865007)" + + BlogSlug -> + "blog" + + BlogNotFoundThing -> + "Blog post" + + BlogHomeTitle -> + "Our Latest Thoughts" + + BlogHomeReadMoreLink -> + "Read more of our blog" + + BlogIndexTitle -> + "Blog" + + BlogMetaDescription -> + "Blog desription" + + BlogCardReadMoreLink -> + "Read more" + + BlogByLineBy -> + "By " + + BlogByLineOn -> + " on " + + NotFoundTitle -> + "Page not found" + + NotFoundThing -> + "Page" + + NotFoundDescription1 thing -> + "The " ++ thing ++ " you were looking for does not exist" + + NotFoundDescription2 -> + "Please check the URL and try again" diff --git a/src/Main.elm b/src/Main.elm index 59df401..f1b0920 100644 --- a/src/Main.elm +++ b/src/Main.elm @@ -1,5 +1,8 @@ module Main exposing (main) +-- import Json.Decode.Field as Field + +import Array exposing (Array) import Browser import Browser.Dom import Browser.Events @@ -8,12 +11,15 @@ import Copy.CaseStudy import Copy.Keys exposing (Key(..)) import Copy.Text exposing (t) import Html.Styled exposing (Html, toUnstyled) +import Json.Decode as JD import MetaTags -import Model exposing (Model) +import Model exposing (BlogPost, Model, PageResource) import Msg exposing (Msg(..)) import Page.AboutUs +import Page.Blog as Blog import Page.CaseStudy import Page.Index +import Page.NotFound import Route exposing (Route(..)) import Set import Task @@ -22,10 +28,11 @@ import Url type alias Flags = - () + { posts : Array BlogPost + } -main : Program Flags Model Msg +main : Program JD.Value Model Msg main = Browser.application { init = init @@ -37,12 +44,66 @@ main = } -init : Flags -> Url.Url -> Browser.Navigation.Key -> ( Model, Cmd Msg ) -init _ url key = +setupResourceForPage : Array BlogPost -> Route -> PageResource +setupResourceForPage blogPosts route = + let + baseResource = + Model.emptyPageResource + in + case route of + Index -> + { baseResource + | meta = MetaTags.metaForRoot + } + + AboutUs -> + { baseResource + | meta = MetaTags.metaForAboutUs + } + + CaseStudy slug -> + case Copy.CaseStudy.caseStudyFromSlug slug of + Just foundCaseStudy -> + { baseResource + | caseStudy = Just foundCaseStudy + , meta = MetaTags.metaForCaseStudy foundCaseStudy + } + + Nothing -> + { baseResource + | meta = MetaTags.metaForNotFound (t CaseStudyTitle) + } + + BlogIndex -> + { baseResource + | meta = MetaTags.metaForBlogIndex + } + + BlogShowPost slug -> + case Blog.findBlogFromSlug blogPosts slug of + Just blogPost -> + { baseResource + | blogPost = Just blogPost + , meta = MetaTags.metaForBlogShowPost blogPost + } + + Nothing -> + { baseResource + | meta = MetaTags.metaForNotFound (t BlogNotFoundThing) + } + + NotFound -> + { baseResource + | meta = MetaTags.metaForNotFound (t NotFoundThing) + } + + +init : JD.Value -> Url.Url -> Browser.Navigation.Key -> ( Model, Cmd Msg ) +init rawFlags url key = let route : Route route = - Maybe.withDefault Index <| Route.fromUrl url + Maybe.withDefault NotFound <| Route.fromUrl url openSections : Set.Set String openSections = @@ -52,14 +113,25 @@ init _ url key = Nothing -> Set.empty + + flags = + rawFlags + |> JD.decodeValue flagsDecoder + |> Result.mapError (\err -> Debug.log "flags decode error: " err) + |> Result.withDefault { posts = Array.empty } + + resource = + setupResourceForPage flags.posts route in ( { key = key , page = route , viewportHeightWidth = ( 800, 800 ) , openSections = openSections + , blogPosts = flags.posts + , pageResource = resource } , Cmd.batch - [ MetaTags.setMetadata <| MetaTags.metaForPage route + [ MetaTags.setMetadata <| resource.meta , Task.perform GotViewport Browser.Dom.getViewport ] ) @@ -109,7 +181,7 @@ update msg model = newRoute = -- If not a valid route, go to index -- could 404 instead depends on desired behaviour - Maybe.withDefault Index (Route.fromUrl url) + Maybe.withDefault NotFound (Route.fromUrl url) openSections : Set.Set String openSections = @@ -119,10 +191,17 @@ update msg model = Nothing -> Set.empty + + resource = + setupResourceForPage model.blogPosts newRoute in - ( { model | page = newRoute, openSections = openSections } + ( { model + | page = newRoute + , openSections = openSections + , pageResource = resource + } , Cmd.batch - [ MetaTags.setMetadata <| MetaTags.metaForPage newRoute + [ MetaTags.setMetadata <| resource.meta , possiblyScrollToTop url ] ) @@ -155,35 +234,67 @@ subscriptions _ = viewDocument : Model -> Browser.Document Msg viewDocument model = - { title = MetaTags.titleForPage model.page + { title = model.pageResource.meta.title , body = [ toUnstyled (view model) ] } view : Model -> Html Msg view model = - case model.page of - Index -> - Theme.View.viewPageWrapper (t SiteTitle) Page.Index.view + let + innerPage = + case model.page of + Index -> + Page.Index.view model.blogPosts + + AboutUs -> + Page.AboutUs.view model + + CaseStudy _ -> + -- FIXME. this is jank + -- we know case study exists + -- we should move the meta data out of the embedded CaseStudy + -- and just pass that along + case model.pageResource.caseStudy of + Just caseStudy -> + caseStudy.maybePageContent + |> Maybe.withDefault Model.emptyCaseStudyContent + |> Page.CaseStudy.view caseStudy.title - AboutUs -> - Theme.View.viewPageWrapper (t AboutUsTitle) (Page.AboutUs.view model) + Nothing -> + Page.NotFound.view (t CaseStudyTitle) - CaseStudy slug -> - let - caseStudy : Model.CaseStudy - caseStudy = - Copy.CaseStudy.caseStudyIdFromSlug slug - |> Copy.CaseStudy.caseStudyFromId - - maybeContent : Maybe Model.CaseStudyContent - maybeContent = - caseStudy.maybePageContent - in - case maybeContent of - Just content -> - Theme.View.viewPageWrapper caseStudy.title (Page.CaseStudy.view caseStudy.title content) + BlogIndex -> + Blog.viewBlogIndex model - Nothing -> - -- Replace with global 404 ? - Theme.View.viewPageWrapper (t SiteTitle) Page.Index.view + BlogShowPost _ -> + case model.pageResource.blogPost of + Just post -> + Blog.viewShowBlogPost post + + Nothing -> + Page.NotFound.view (t BlogNotFoundThing) + + NotFound -> + Page.NotFound.view (t NotFoundThing) + in + Theme.View.viewPageWrapper + model.pageResource.meta.title + innerPage + + +flagsDecoder : JD.Decoder Flags +flagsDecoder = + let + postDecoder = + JD.map7 BlogPost + (JD.field "slug" JD.string) + (JD.field "author" JD.string) + (JD.field "publish_date" JD.string) + (JD.field "title" JD.string) + (JD.field "teaser" JD.string) + (JD.field "keywords" JD.string) + (JD.field "content" JD.string) + in + JD.map Flags + (JD.field "blog_posts" <| JD.array postDecoder) diff --git a/src/MetaTags.elm b/src/MetaTags.elm index 4b79978..ff8c68d 100644 --- a/src/MetaTags.elm +++ b/src/MetaTags.elm @@ -1,65 +1,79 @@ -port module MetaTags exposing (PageMetadata, metaForPage, setMetadata, titleForPage) +port module MetaTags exposing + ( metaForAboutUs + , metaForBlogIndex + , metaForBlogShowPost + , metaForCaseStudy + , metaForNotFound + , metaForRoot + , setMetadata + ) -import Copy.CaseStudy import Copy.Keys as Keys import Copy.Text exposing (t) -import Route +import Model exposing (PageMetadata) -type alias PageMetadata = - { title : String - , description : String - , url : Maybe String - , imageSrc : Maybe String +metaForRoot : PageMetadata +metaForRoot = + { title = t <| Keys.WindowTitle <| t Keys.HomeTitle + , description = t Keys.HomeMetaDescription + , url = Nothing + , imageSrc = Nothing } -titleForPage : Route.Route -> String -titleForPage route = - case route of - Route.AboutUs -> - t Keys.AboutUsTitle ++ " - " ++ t Keys.SiteTitle - - _ -> - t Keys.SiteTitle - - -metaForPage : Route.Route -> PageMetadata -metaForPage route = - case route of - Route.Index -> - { title = titleForPage route - , description = t Keys.HomeMetaDescription - , url = Nothing - , imageSrc = Nothing - } - - Route.AboutUs -> - { title = titleForPage route - , description = t Keys.AboutUsMetaDescription - , url = Nothing - , imageSrc = Nothing - } - - Route.CaseStudy slug -> - let - caseStudy = - Copy.CaseStudy.caseStudyIdFromSlug slug - |> Copy.CaseStudy.caseStudyFromId - - metaDescription = - case caseStudy.maybePageContent of - Just pageContent -> - pageContent.metaDescription - - Nothing -> - t Keys.HomeMetaDescription - in - { title = caseStudy.title - , description = metaDescription - , url = caseStudy.metaUrl - , imageSrc = caseStudy.metaImageSrc - } +metaForAboutUs : PageMetadata +metaForAboutUs = + { title = t <| Keys.WindowTitle <| t Keys.AboutUsTitle + , description = t Keys.AboutUsMetaDescription + , url = Nothing + , imageSrc = Nothing + } + + +metaForCaseStudy : Model.CaseStudy -> PageMetadata +metaForCaseStudy caseStudy = + let + metaDescription = + case caseStudy.maybePageContent of + Just pageContent -> + pageContent.metaDescription + + Nothing -> + t Keys.HomeMetaDescription + in + { title = t <| Keys.WindowTitle <| caseStudy.title -- could be caseStudie.title ++ " - " ++ t Keys.CaseStudyTitle + , description = metaDescription + , url = caseStudy.metaUrl + , imageSrc = caseStudy.metaImageSrc + } + + +metaForBlogIndex : PageMetadata +metaForBlogIndex = + { title = t <| Keys.WindowTitle <| t Keys.BlogIndexTitle + , description = t Keys.BlogMetaDescription + , url = Nothing + , imageSrc = Nothing + } + + +metaForBlogShowPost : Model.BlogPost -> PageMetadata +metaForBlogShowPost post = + { title = t <| Keys.WindowTitle <| post.title + , description = post.teaser + , url = Nothing + , imageSrc = Nothing + } + + +metaForNotFound : String -> PageMetadata +metaForNotFound thing = + { title = t <| Keys.WindowTitle <| t Keys.NotFoundTitle + , description = t <| Keys.NotFoundDescription1 thing + , url = Nothing + , imageSrc = Nothing + } port setMetadata : PageMetadata -> Cmd msg diff --git a/src/Model.elm b/src/Model.elm index 556bd56..2b2ab5a 100644 --- a/src/Model.elm +++ b/src/Model.elm @@ -1,16 +1,62 @@ -module Model exposing (CaseStudy, CaseStudyContent, Image, Model, ProfileInfo, Quote) +module Model exposing (BlogPost, CaseStudy, CaseStudyContent, Image, Model, PageMetadata, PageResource, ProfileInfo, Quote, emptyCaseStudy, emptyCaseStudyContent, emptyPageMetadata, emptyPageResource) +import Array exposing (Array) import Browser.Navigation import Copy.Keys import Route exposing (Route) import Set exposing (Set) +type alias PageMetadata = + { title : String + , description : String + , url : Maybe String + , imageSrc : Maybe String + } + + +emptyPageMetadata : PageMetadata +emptyPageMetadata = + { title = "" + , description = "" + , url = Nothing + , imageSrc = Nothing + } + + type alias Model = { key : Browser.Navigation.Key , page : Route , viewportHeightWidth : ( Float, Float ) , openSections : Set String + , blogPosts : Array BlogPost + , pageResource : PageResource + } + + +type alias PageResource = + { blogPost : Maybe BlogPost + , caseStudy : Maybe CaseStudy + , meta : PageMetadata + } + + +emptyPageResource : PageResource +emptyPageResource = + { blogPost = Nothing + , caseStudy = Nothing + , meta = emptyPageMetadata + } + + +type alias BlogPost = + { slug : String + , author : String + , publishDate : String -- YYYY-MM-DD + , title : String + , teaser : String + , keywords : String + , content : String } @@ -41,6 +87,21 @@ type alias CaseStudy = } +emptyCaseStudy : CaseStudy +emptyCaseStudy = + { name = "" + , title = "" + , teaserBackgroundSrc = "" + , teaserSummary = "" + , teaserLinkText = "" + , teaserHref = "" + , maybePageContent = Nothing + , metaTitle = "" + , metaUrl = Nothing + , metaImageSrc = Nothing + } + + type alias CaseStudyContent = { introMarkdown : String , metaDescription : String @@ -52,6 +113,18 @@ type alias CaseStudyContent = } +emptyCaseStudyContent : CaseStudyContent +emptyCaseStudyContent = + { introMarkdown = "" + , metaDescription = "" + , maybeIntroImage = Nothing + , whatWeDidMarkdown = "" + , maybeWhatWeDidImage = Nothing + , resultsMarkdown = "" + , maybeQuote = Nothing + } + + type alias ProfileInfo = { section : Copy.Keys.Section , name : String diff --git a/src/Page/Blog.elm b/src/Page/Blog.elm new file mode 100644 index 0000000..10d4a24 --- /dev/null +++ b/src/Page/Blog.elm @@ -0,0 +1,166 @@ +module Page.Blog exposing (blogCard, findBlogFromSlug, viewBlogCardPromo, viewBlogIndex, viewShowBlogPost) + +import Array exposing (Array) +import Copy.Keys exposing (Key(..), Section(..)) +import Copy.Text exposing (t) +import Css exposing (..) +import Html.Styled exposing (Html, a, div, h1, h3, p, text) +import Html.Styled.Attributes exposing (css, href) +import Model exposing (BlogPost, Model) +import Msg exposing (Msg) +import Route +import Theme.Style exposing (green, pink, shadow, withMediaTablet) +import Theme.View exposing (contentContainer) + + +findBlogFromSlug : Array Model.BlogPost -> String -> Maybe Model.BlogPost +findBlogFromSlug blogPosts slug = + blogPosts + |> Array.filter (\p -> p.slug == slug) + |> Array.get 0 + + +viewBlogIndex : Model -> Html Msg +viewBlogIndex model = + div [] + [ -- title + div [ css [ titleBackgroundStyle ] ] + [ div [ css [ contentContainer, titleAreaStyle ] ] + [ h1 [ css [ titleStyle ] ] [ text <| t BlogIndexTitle ] + ] + ] + , div [ css [ contentContainer, contentAreaStyle ] ] + (model.blogPosts + |> Array.toList + |> List.map + (\post -> + div [ css [ blogCardStyle ] ] + [ blogCard post ] + ) + ) + ] + + +viewShowBlogPost : Model.BlogPost -> Html Msg +viewShowBlogPost post = + div [] + [ -- title + div [ css [ titleBackgroundStyle ] ] + [ div [ css [ contentContainer, titleAreaStyle ] ] + [ h1 [ css [ titleStyle ] ] [ text post.title ] + , byLine post + ] + ] + + -- content + , div [ css [ contentContainer, contentAreaStyle ] ] + [ Theme.View.markdownToHtml post.content + ] + ] + + +blogCard : BlogPost -> Html Msg +blogCard post = + div [] + [ h3 [] [ text post.title ] + , byLine post + , p [] [ text post.teaser ] + , p [] [ a [ href <| Route.toString <| Route.BlogShowPost post.slug ] [ text <| t BlogCardReadMoreLink ] ] + ] + + +byLine : BlogPost -> Html Msg +byLine post = + p [ css [ byLineStyle ] ] + [ text <| t BlogByLineBy + , Html.Styled.em [] [ text post.author ] + , text <| t BlogByLineOn + , Html.Styled.em [] [ text post.publishDate ] + ] + + +capArrayLength : Int -> Array v -> Array v +capArrayLength max array = + if Array.length array > max then + Array.slice 0 max array + + else + array + + +viewBlogCardPromo : Array BlogPost -> Html Msg +viewBlogCardPromo blogPosts = + -- as seen on the homepage + blogPosts + |> capArrayLength 3 + |> Array.toList + |> List.map blogCard + |> div [ css [ blogCardPromoAreaStyle ] ] + + + +-- Styles + + +titleBackgroundStyle : Style +titleBackgroundStyle = + batch + [ backgroundColor pink.light + , boxShadow4 (px 0) (px 0) (px 20) shadow + , withMediaTablet + [ padding2 (rem 2) (rem 1) + ] + ] + + +titleAreaStyle : Style +titleAreaStyle = + batch + [ padding2 (rem 2) (rem 1) + ] + + +titleStyle : Style +titleStyle = + batch + [ fontSize (rem 2.5) + , fontWeight bold + , lineHeight (rem 3) + , withMediaTablet + [ fontSize (rem 3.75) + , lineHeight (rem 4.5) + ] + ] + + +contentAreaStyle : Style +contentAreaStyle = + batch + [ padding2 (rem 2) (rem 1) + ] + + +blogCardStyle : Style +blogCardStyle = + batch + [ paddingBottom (rem 2) + ] + + +blogCardPromoAreaStyle : Style +blogCardPromoAreaStyle = + batch + [ displayFlex + , maxWidth (px 1000) + , margin auto + , property "gap" "2em" + , justifyContent center + ] + + +byLineStyle : Style +byLineStyle = + batch + [ fontSize (Css.em 0.9) + , color green.dark + ] diff --git a/src/Page/Index.elm b/src/Page/Index.elm index b464c93..311bb16 100644 --- a/src/Page/Index.elm +++ b/src/Page/Index.elm @@ -1,7 +1,8 @@ module Page.Index exposing (view) +import Array exposing (Array) import Copy.AboutUs -import Copy.CaseStudy exposing (CaseStudyKey(..)) +import Copy.CaseStudy import Copy.Keys exposing (Key(..)) import Copy.Text exposing (t) import Css exposing (..) @@ -9,26 +10,36 @@ import Html.Styled exposing (Html, a, div, h1, h2, img, li, p, section, text, ul import Html.Styled.Attributes exposing (alt, class, css, href, src) import Model import Msg exposing (Msg) -import Theme.Style exposing (fuchsia, pink, shadow, white, withMediaTablet) +import Page.Blog as Blog +import Route +import Theme.Style exposing (fuchsia, green, pink, shadow, white, withMediaTablet) import Theme.View exposing (generateId) -featuredCaseStudyList : List Copy.CaseStudy.CaseStudyKey +featuredCaseStudyList : List Model.CaseStudy featuredCaseStudyList = - [ Foyer, CodeReadingClub, NewProjectInvite ] + [ Copy.CaseStudy.foyer + , Copy.CaseStudy.codeReadingClub + , Copy.CaseStudy.newProjectInvite + ] -view : Html Msg -view = +view : Array Model.BlogPost -> Html Msg +view blogPosts = div [] - [ section [ css [ sectionStyle ], class "home-section" ] + [ -- hero banner + section [ css [ sectionStyle ], class "home-section" ] [ h1 [ css [ sectionHeadingStyle ] ] [ text (t WhatWeDoHeading) ] , Theme.View.markdownToHtml (t WhatWeDoMarkdown) ] + + -- case studies , section [ css [ sectionStyle, sectionHighlightStyle ], class "home-section" ] [ h2 [ css [ sectionHeadingStyle ] ] [ text (t ThingsWeWorkOnHeading) ] , ul [ css [ workListStyle ] ] (viewWorkingOnList featuredCaseStudyList) ] + + -- who we are , section [ css [ sectionStyle ], class "home-section" ] [ h2 [ css [ sectionHeadingStyle ] ] [ text (t WhoWeAreHeading) ] , Theme.View.markdownToHtml (t WhoWeAreMarkdown1) @@ -37,21 +48,23 @@ view = (viewWhoWeAreList Copy.AboutUs.profiles) , Theme.View.markdownToHtml (t WhoWeAreMarkdown2) ] + + -- blog sample + , section [ css [ sectionStyle, sectionAltHighlightStyle ], class "home-section" ] + [ h2 [ css [ sectionHeadingStyle ] ] [ text <| t BlogHomeTitle ] + , Blog.viewBlogCardPromo blogPosts + , p [] [ a [ href <| Route.toString Route.BlogIndex ] [ text <| t BlogHomeReadMoreLink ] ] + ] ] -viewWorkingOnList : List Copy.CaseStudy.CaseStudyKey -> List (Html Msg) +viewWorkingOnList : List Model.CaseStudy -> List (Html Msg) viewWorkingOnList featuredCaseStudies = - List.map - (\caseStudyId -> - let - caseStudy : Model.CaseStudy - caseStudy = - Copy.CaseStudy.caseStudyFromId caseStudyId - in - li [ css [ workingOnCardStyle ] ] (viewCaseStudyCard caseStudy) - ) - featuredCaseStudies + featuredCaseStudies + |> List.map + (\caseStudy -> + li [ css [ workingOnCardStyle ] ] (viewCaseStudyCard caseStudy) + ) viewCaseStudyCard : Model.CaseStudy -> List (Html Msg) @@ -121,6 +134,14 @@ sectionHighlightStyle = ] +sectionAltHighlightStyle : Style +sectionAltHighlightStyle = + batch + [ backgroundColor green.light + , boxShadow4 (px 0) (px 0) (px 20) shadow + ] + + sectionHeadingStyle : Style sectionHeadingStyle = batch @@ -146,6 +167,13 @@ workListStyle = ] +writingListStyle : Style +writingListStyle = + batch + [ displayFlex + ] + + workingOnCardStyle : Style workingOnCardStyle = batch diff --git a/src/Page/NotFound.elm b/src/Page/NotFound.elm new file mode 100644 index 0000000..9cd1399 --- /dev/null +++ b/src/Page/NotFound.elm @@ -0,0 +1,46 @@ +module Page.NotFound exposing (view) + +import Copy.Keys exposing (Key(..)) +import Copy.Text exposing (t) +import Css exposing (..) +import Html.Styled exposing (Html, div, h1, p, text) +import Html.Styled.Attributes exposing (css) +import Msg exposing (Msg) +import Theme.Style exposing (pink, shadow) + + +view : String -> Html Msg +view thing = + div [] + [ div [ css [ titleSectionStyle ] ] + [ h1 [] [ text <| t NotFoundTitle ] + ] + , div [ css [ contentAreaStyle ] ] + [ p [] [ text <| t <| NotFoundDescription1 thing ] + , p [] [ text <| t NotFoundDescription2 ] + ] + ] + + + +-- Styles + + +titleSectionStyle : Style +titleSectionStyle = + batch + [ backgroundColor pink.light + , boxShadow5 (px 0) (px 12) (px 20) (px -8) shadow + , color pink.dark + , padding2 (rem 2) zero + , textAlign center + ] + + +contentAreaStyle : Style +contentAreaStyle = + batch + [ padding2 (rem 2) (rem 1) + , textAlign center + , fontSize (em 1.5) + ] diff --git a/src/Route.elm b/src/Route.elm index a240a47..e216a91 100644 --- a/src/Route.elm +++ b/src/Route.elm @@ -10,6 +10,9 @@ type Route = Index | AboutUs | CaseStudy String + | BlogIndex + | BlogShowPost String + | NotFound fromUrl : Url.Url -> Maybe Route @@ -30,6 +33,15 @@ toString route = CaseStudy slug -> "/" ++ t CaseStudySlug ++ "/" ++ slug + BlogIndex -> + "/" ++ t BlogSlug + + BlogShowPost slug -> + "/" ++ t BlogSlug ++ "/" ++ slug + + NotFound -> + "/" + routeParser : Parser (Route -> a) a routeParser = @@ -37,4 +49,6 @@ routeParser = [ map Index top , map AboutUs (s (t AboutUsSlug)) , map CaseStudy (s (t CaseStudySlug) string) + , map BlogIndex (s (t BlogSlug)) + , map BlogShowPost (s (t BlogSlug) string) ] diff --git a/src/assets/blog-data.json b/src/assets/blog-data.json new file mode 100644 index 0000000..e8ff492 --- /dev/null +++ b/src/assets/blog-data.json @@ -0,0 +1 @@ +[{"title":"Introducing the Cookie Blog","author":"Ivan Kocienski","teaser":"A little hello from us with an introduction to our plans for the coming year.","keywords":"introduction, thoughts, plan","slug":"introducing-the-cookie-blog","publish_date":"2026-02-14","content":"\nHello!\n\nAnd welcome to the cookiewolf blog- our little slice of the internet where we can post ideas and updates about all the things we are getting up to. (Or that is the plan!)\n\nWe are an active group in our local communities so it will be nice to take some of those experiences and put them into words.\n\nWe also hope this will be where we can share resources with the wider community.\n\nOr maybe one week we just want to share our cookie recipes.\n\nWith love,\n\nThe Cookie Wolf humans.\n"},{"title":"Escaping Big Tech","author":"Ivan Kocienski","teaser":"BigTech: over-dependence on centralised platforms","keywords":"introduction, thoughts, plan","slug":"escaping-big-tech","publish_date":"2026-02-01","content":"\n## Introduction\n\nApple, Microsoft, Meta, Google, etc. are increasing their grip on more of our lives, both professional and personal. The problem is they're very convenient and have well established integration in our day-to-day activities.\n\nI think worrying about such things can seem unecassery and as long as our rights (and their legal obligations) are followed then there is nothing to worry about?\n\nBut it doesn't take a lot of looking around to find examples of Big Tech abusing its status as a defacto monopoly and how they only seem happy to protect our rights when it aligns with their profit margins and quarterly performance reviews.\n\n## You are not alone\n\n[RABT]\n\n## So what can we do?\n\nTime to take our data back into our own hands. Maybe not overnight, but just a little, here and there.\n\n## Our tech stack\n\nAt Cookiewolf we try to avoid as much of this as possible. Here are a few bits of tech we use\n- [ghandi]() DNS\n- [Hetzner]() VPS hosting\n- [Netlify]() hosting\n- [cloudron]() platform\n- [whereby]() video conferencing\n- [zulip]() chat platform\n- [linux]() OS\n\n## We can do better\n\nUnfortunately a big part of our day-to-day software engineering efforts take place on Microsofts Github. We have all our repositories there and in turn all our bug tracking. Moving will not be easy.\n\nWe hope to move to [gitlab] (either self hosted or as a paid PaaS) in 2026.\n\n## Conclusion\n\nI hope"}] \ No newline at end of file diff --git a/src/assets/main.js b/src/assets/main.js index 2524547..c4e1665 100644 --- a/src/assets/main.js +++ b/src/assets/main.js @@ -2,6 +2,7 @@ import './reset.css'; import { Elm } from '../Main.elm'; import '@fontsource/poppins/400.css'; import '@fontsource/poppins/800.css'; +import blogData from './blog-data.json'; if (process.env.NODE_ENV === 'development') { const ElmDebugTransform = await import('elm-debug-transformer'); @@ -11,8 +12,11 @@ if (process.env.NODE_ENV === 'development') { }); } +const flags = { blog_posts: blogData }; +console.log("JS: flags=", flags); + const root = document.querySelector('#app div'); -const app = Elm.Main.init({ node: root }); +const app = Elm.Main.init({ node: root, flags: flags }); app.ports.setMetadata.subscribe(function (pageMetadata) { const baseUrl = From 93d449f715494b4ce9373213428c46967b04fe79 Mon Sep 17 00:00:00 2001 From: Ivan Kocienski Date: Mon, 19 Jan 2026 09:54:19 +0000 Subject: [PATCH 2/8] remove Debug log --- src/Main.elm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Main.elm b/src/Main.elm index f1b0920..ebb96fa 100644 --- a/src/Main.elm +++ b/src/Main.elm @@ -117,7 +117,7 @@ init rawFlags url key = flags = rawFlags |> JD.decodeValue flagsDecoder - |> Result.mapError (\err -> Debug.log "flags decode error: " err) + --|> Result.mapError (\err -> Debug.log "flags decode error: " err) |> Result.withDefault { posts = Array.empty } resource = From 70aa303135f02061bc2e08f4b01afc8669516f76 Mon Sep 17 00:00:00 2001 From: Ivan Kocienski Date: Mon, 19 Jan 2026 11:17:16 +0000 Subject: [PATCH 3/8] fix styling of blog bit on index for mobile devices --- src/Page/Blog.elm | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Page/Blog.elm b/src/Page/Blog.elm index 10d4a24..cfe27a8 100644 --- a/src/Page/Blog.elm +++ b/src/Page/Blog.elm @@ -151,10 +151,16 @@ blogCardPromoAreaStyle : Style blogCardPromoAreaStyle = batch [ displayFlex - , maxWidth (px 1000) + , flexDirection column + , fontSize (rem 1) , margin auto - , property "gap" "2em" - , justifyContent center + , maxWidth (px 1000) + , padding zero + , Css.property "gap" "2rem" + , withMediaTablet + [ flexDirection row + , Css.property "gap" "0" + ] ] From bfc06ad7b2f0f9c084389cc59fcdf4b03ecae9ca Mon Sep 17 00:00:00 2001 From: Ivan Kocienski Date: Tue, 27 Jan 2026 11:18:55 +0000 Subject: [PATCH 4/8] Blog RSS feed --- README.md | 4 +- index.html | 11 +++- package.json | 2 +- scripts/convert-blog-posts-to-json-payload.js | 59 +++++++++++++++++-- src/assets/blog-feed.xml | 27 +++++++++ 5 files changed, 92 insertions(+), 11 deletions(-) create mode 100644 src/assets/blog-feed.xml diff --git a/README.md b/README.md index f130582..0c44dcf 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,9 @@ Then run `npm run build_blog`. This will validate that all post files have the c meta fields and output a `blog-data.json` payload. You can then reload your local server and the new post should appear at the front of the list. -Any added posts and changes to the `blog-data.json` file should be pushed to main branch +There is also the RSS feed output file written to `blog-feed.xml`. + +Any added posts and changes to the payload or feed files should be pushed to main branch to make live on the public site. The meta fields are (currently): diff --git a/index.html b/index.html index 447167e..c61d1fd 100644 --- a/index.html +++ b/index.html @@ -9,13 +9,18 @@ Cookiewolf Co-op - - + + - + + + diff --git a/package.json b/package.json index 02791cc..41bc924 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "format": "elm-format src/ --yes", "lint": "elm-review", "test": "elm-test-rs", - "build_blog": "node scripts/convert-blog-posts-to-json-payload.js blog-posts/ src/assets/blog-data.json" + "build_blog": "node scripts/convert-blog-posts-to-json-payload.js blog-posts/ src/assets/blog-data.json src/assets/blog-feed.xml" }, "devDependencies": { "elm-review": "^2.13.2", diff --git a/scripts/convert-blog-posts-to-json-payload.js b/scripts/convert-blog-posts-to-json-payload.js index 6cae5ad..718ba7a 100644 --- a/scripts/convert-blog-posts-to-json-payload.js +++ b/scripts/convert-blog-posts-to-json-payload.js @@ -2,10 +2,13 @@ Don't run this script directly, use `npm run build_blog`! -scripts/convert-blog-posts-to-json-payload.js +scripts/convert-blog-posts-to-json-payload.js Looks for markdown files in src directory and bundles all that data into one json file for deployment. + +NOTE: yes i know this is a bit verbose as it does everything manually but it +also has ZERO external dependencies, which makes distribution simpler (i hope). */ const fs = require('node:fs/promises'); @@ -160,18 +163,59 @@ function savePostData(postData, targetFile) { */ } +function saveFeed(postData, targetFile) { + + let postList = + postData + .map( + post => + ` + ${post.title} + ${post.teaser} + https://cookiewolf.coop/blog/${post.slug} + ${post.slug} + ${post.publish_date}T12:00:00.000Z + `) + .join("\n"); + + const builtOn = new Date().toISOString(); + + const output = + ` + + + Cookiewolf blog + The place for cookiewolf thoughts of all kinds. + https://cookiewolf.coop + Cookiewolf Coop + ${builtOn} + ${builtOn} + 1800 + + ${postList} + +`; + + // console.log("feed:"); + // console.log(output); + + return fs.writeFile(targetFile, output); +} + function main() { - if (process.argv.length < 4) { - console.error("Missing src / output arguments"); + if (process.argv.length < 5) { + console.error("Missing src / output / feed output arguments"); process.exit(-1); } const srcDir = process.argv[2]; - const targetFile = process.argv[3]; + const targetPayloadFile = process.argv[3]; + const targetFeedFile = process.argv[4]; console.log('convert-blog-posts-to-json-payload'); console.log(" srcDir=" + srcDir); - console.log(" targetFile=" + targetFile) + console.log(" targetPayloadFile=" + targetPayloadFile) + console.log(" targetFeedFile=" + targetFeedFile) fs.readdir(srcDir) .then(files => { @@ -188,7 +232,10 @@ function main() { Promise .all(postFileData) .then(normalizePostData) - .then(data => savePostData(data, targetFile)) + .then(data => + Promise.all( + [savePostData(data, targetPayloadFile), saveFeed(data, targetFeedFile)] + )) .then(() => { console.log("all done."); }) diff --git a/src/assets/blog-feed.xml b/src/assets/blog-feed.xml new file mode 100644 index 0000000..fbcabc6 --- /dev/null +++ b/src/assets/blog-feed.xml @@ -0,0 +1,27 @@ + + + + Cookiewolf blog + The place for cookiewolf thoughts of all kinds. + https://cookiewolf.coop + Cookiewolf Coop + 2026-01-27T11:11:40.785Z + 2026-01-27T11:11:40.785Z + 1800 + + + Introducing the Cookie Blog + A little hello from us with an introduction to our plans for the coming year. + https://cookiewolf.coop/blog/introducing-the-cookie-blog + introducing-the-cookie-blog + 2026-02-14T12:00:00.000Z + + + Escaping Big Tech + BigTech: over-dependence on centralised platforms + https://cookiewolf.coop/blog/escaping-big-tech + escaping-big-tech + 2026-02-01T12:00:00.000Z + + + \ No newline at end of file From e35bde929a1fc74e6f53a14afab72e50a7628b58 Mon Sep 17 00:00:00 2001 From: Ivan Kocienski Date: Tue, 27 Jan 2026 11:47:19 +0000 Subject: [PATCH 5/8] use .rss for filename --- README.md | 2 +- index.html | 2 +- package.json | 2 +- src/assets/{blog-feed.xml => blog-feed.rss} | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) rename src/assets/{blog-feed.xml => blog-feed.rss} (89%) diff --git a/README.md b/README.md index 0c44dcf..55f6ce4 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ Then run `npm run build_blog`. This will validate that all post files have the c meta fields and output a `blog-data.json` payload. You can then reload your local server and the new post should appear at the front of the list. -There is also the RSS feed output file written to `blog-feed.xml`. +There is also the RSS feed output file written to `blog-feed.rss`. Any added posts and changes to the payload or feed files should be pushed to main branch to make live on the public site. diff --git a/index.html b/index.html index c61d1fd..1d6916d 100644 --- a/index.html +++ b/index.html @@ -20,7 +20,7 @@ content="Cookiewolf is a cooperatively-run digital development and design studio. We work with organisations of all sizes to discover, design and build the digital tools they need."> - + diff --git a/package.json b/package.json index 41bc924..d8aaf2c 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "format": "elm-format src/ --yes", "lint": "elm-review", "test": "elm-test-rs", - "build_blog": "node scripts/convert-blog-posts-to-json-payload.js blog-posts/ src/assets/blog-data.json src/assets/blog-feed.xml" + "build_blog": "node scripts/convert-blog-posts-to-json-payload.js blog-posts/ src/assets/blog-data.json src/assets/blog-feed.rss" }, "devDependencies": { "elm-review": "^2.13.2", diff --git a/src/assets/blog-feed.xml b/src/assets/blog-feed.rss similarity index 89% rename from src/assets/blog-feed.xml rename to src/assets/blog-feed.rss index fbcabc6..d8bc0dd 100644 --- a/src/assets/blog-feed.xml +++ b/src/assets/blog-feed.rss @@ -5,8 +5,8 @@ The place for cookiewolf thoughts of all kinds. https://cookiewolf.coop Cookiewolf Coop - 2026-01-27T11:11:40.785Z - 2026-01-27T11:11:40.785Z + 2026-01-27T11:46:25.190Z + 2026-01-27T11:46:25.190Z 1800 From e79417625ef43e639419ae5e0cd5edaaf0db9887 Mon Sep 17 00:00:00 2001 From: Ivan Kocienski Date: Tue, 27 Jan 2026 11:58:07 +0000 Subject: [PATCH 6/8] try removing / to see if this fixes weird inline rss situation --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index 1d6916d..cf65b51 100644 --- a/index.html +++ b/index.html @@ -20,7 +20,7 @@ content="Cookiewolf is a cooperatively-run digital development and design studio. We work with organisations of all sizes to discover, design and build the digital tools they need."> - + From 0b45270c3d2c13e910dec5acb269e6a331dcaba7 Mon Sep 17 00:00:00 2001 From: Ivan Kocienski Date: Wed, 28 Jan 2026 10:34:15 +0000 Subject: [PATCH 7/8] try moving blog feed RSS file to folder --- index.html | 2 +- {src/assets => public}/blog-feed.rss | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename {src/assets => public}/blog-feed.rss (100%) diff --git a/index.html b/index.html index cf65b51..34b1b8b 100644 --- a/index.html +++ b/index.html @@ -20,7 +20,7 @@ content="Cookiewolf is a cooperatively-run digital development and design studio. We work with organisations of all sizes to discover, design and build the digital tools they need."> - + diff --git a/src/assets/blog-feed.rss b/public/blog-feed.rss similarity index 100% rename from src/assets/blog-feed.rss rename to public/blog-feed.rss From b849c25379fd4217d40c4456c13f6e89cb7e02af Mon Sep 17 00:00:00 2001 From: Ivan Kocienski Date: Wed, 28 Jan 2026 10:54:51 +0000 Subject: [PATCH 8/8] updated build_blog script to output to correct file --- package.json | 2 +- public/blog-feed.rss | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index d8aaf2c..d75cb75 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "format": "elm-format src/ --yes", "lint": "elm-review", "test": "elm-test-rs", - "build_blog": "node scripts/convert-blog-posts-to-json-payload.js blog-posts/ src/assets/blog-data.json src/assets/blog-feed.rss" + "build_blog": "node scripts/convert-blog-posts-to-json-payload.js blog-posts/ src/assets/blog-data.json public/blog-feed.rss" }, "devDependencies": { "elm-review": "^2.13.2", diff --git a/public/blog-feed.rss b/public/blog-feed.rss index d8bc0dd..3146b2a 100644 --- a/public/blog-feed.rss +++ b/public/blog-feed.rss @@ -5,8 +5,8 @@ The place for cookiewolf thoughts of all kinds. https://cookiewolf.coop Cookiewolf Coop - 2026-01-27T11:46:25.190Z - 2026-01-27T11:46:25.190Z + 2026-01-28T10:54:08.551Z + 2026-01-28T10:54:08.551Z 1800