diff --git a/priv/styles/main.css b/priv/styles/main.css index 9a01f7cf..31b9eafe 100644 --- a/priv/styles/main.css +++ b/priv/styles/main.css @@ -1372,3 +1372,87 @@ pre code { .tooltip-container[data-tooltip-trigger="hover"]:hover .tooltip, .tooltip-container[data-tooltip-state="open"] .tooltip { opacity: 1; } + +/* Table of Contents layout */ +.toc-layout { + max-width: 1200px; + margin: 0 auto; + display: grid; + grid-template-columns: minmax(auto, 340px) auto; + gap: 1.6rem; + padding: 0 var(--gap-2) 2rem; +} + +/* This just gives us a nice bit of padding atop the headings */ +.toc-layout .prose h1, +.toc-layout .prose h2 { + scroll-margin-top: 1em; +} + +/* Hide the table of contents on mobile, it is very impracticle and bulky there. */ +@media (max-width: 960px) { + .toc-layout { + grid-template-columns: auto; + } + + .toc-layout .table-of-contents { + display: none; + } +} + +.table-of-contents { + position: sticky; + top: 16px; + background: var(--color-lukewarm-charcoal); + border: 1px solid var(--color-warm-charcoal); + overflow-y: auto; + max-height: 90vh; + border-radius: .4rem; + font-size: 1rem; + padding-bottom: 1.4rem; +} + +.table-of-contents h4 { + margin: 0; + padding: 2rem 1.4rem .4rem; + font-weight: 700; + font-size: 1rem; + color: var(--color-text-subtle); +} + +.table-of-contents ul { + padding: 0; + margin: 0; +} + +.table-of-contents li { + display: block; + position: relative; +} + +.table-of-contents ul ul li:before { + content: ''; + border-bottom: 2px solid var(--color-warm-charcoal); + border-left: 2px solid var(--color-warm-charcoal); + position: absolute; + top: -50%; + bottom: 50%; + width: 1ch; + pointer-events: none; + left: 1.4rem; +} + +.table-of-contents ul ul li:first-child:before { + top: 0; +} + +.table-of-contents ul ul a { + padding-left: 2.8rem; +} + +.table-of-contents a { + text-decoration: none; + padding: .6rem 1.4rem; + color: #EEF1FF; + display: flex; +} diff --git a/src/website/cheatsheet.gleam b/src/website/cheatsheet.gleam index f0bc0962..00f8deee 100644 --- a/src/website/cheatsheet.gleam +++ b/src/website/cheatsheet.gleam @@ -1297,7 +1297,7 @@ let assert #(1 as a, 2 as b) = #(1, 2) ]), ]), ] - |> page.page_layout("roadmap", meta, ctx) + |> page.layout_header("roadmap", meta, ctx) |> page.to_html_file(meta) } @@ -2545,7 +2545,7 @@ pub fn get_id() { html.h3([attr.id("nested-modules")], [html.text("Nested modules")]), html.h3([attr.id("first-class-modules")], [html.text("First class modules")]), ] - |> page.page_layout("roadmap", meta, ctx) + |> page.layout_header("roadmap", meta, ctx) |> page.to_html_file(meta) } @@ -4277,7 +4277,7 @@ pub fn main() { ]), ]), ] - |> page.page_layout("roadmap", meta, ctx) + |> page.layout_header("roadmap", meta, ctx) |> page.to_html_file(meta) } @@ -7006,7 +7006,7 @@ server applications comparable to RabbitMQ or multiplayer game servers.", ]), ]), ] - |> page.page_layout("roadmap", meta, ctx) + |> page.layout_header("roadmap", meta, ctx) |> page.to_html_file(meta) } @@ -8189,7 +8189,7 @@ pub fn main() { ]), ]), ] - |> page.page_layout("roadmap", meta, ctx) + |> page.layout_header("roadmap", meta, ctx) |> page.to_html_file(meta) } @@ -10222,6 +10222,6 @@ string.inspect([1, 2, 3]) == \"[1, 2, 3]\" ]), ]), ] - |> page.page_layout("roadmap", meta, ctx) + |> page.layout_header("roadmap", meta, ctx) |> page.to_html_file(meta) } diff --git a/src/website/command_line_reference.gleam b/src/website/command_line_reference.gleam index e3d292c1..cf963e95 100644 --- a/src/website/command_line_reference.gleam +++ b/src/website/command_line_reference.gleam @@ -472,6 +472,6 @@ pub fn page(ctx: site.Context) -> fs.File { html.text("Update dependency packages to their latest versions"), ]), ] - |> page.page_layout("roadmap", meta, ctx) + |> page.layout_header("roadmap", meta, ctx) |> page.to_html_file(meta) } diff --git a/src/website/language_server.gleam b/src/website/language_server.gleam index 99021e43..4f369c94 100644 --- a/src/website/language_server.gleam +++ b/src/website/language_server.gleam @@ -135,7 +135,7 @@ pub fn page(ctx: site.Context) -> fs.File { table_of_contents, ..content ] - |> page.page_layout("prose", meta, ctx) + |> page.layout_header("prose", meta, ctx) |> page.to_html_file(meta) } diff --git a/src/website/page.gleam b/src/website/page.gleam index 7520fe9f..ac0c092a 100644 --- a/src/website/page.gleam +++ b/src/website/page.gleam @@ -1,10 +1,12 @@ import contour +import gleam/dict import gleam/int import gleam/list import gleam/option.{type Option} import gleam/string import gleam/time/calendar import gleam/time/timestamp +import jot import just/highlight as just import lustre/attribute.{attribute as attr, class} as attr import lustre/element.{type Element} @@ -45,6 +47,272 @@ pub fn redirect(from: String, to: String) -> fs.File { fs.File(path: from, content:) } +pub type ContentLink { + ContentLink(title: String, href: String, children: List(ContentLink)) +} + +pub fn table_of_contents_from_djot(document: jot.Document) -> List(ContentLink) { + document.content + |> list.fold(from: [], over: _, with: fn(accum, block) { + case block { + jot.Heading(attributes:, level: 2, content: [jot.Text(text)]) -> + case dict.get(attributes, "id") { + Ok(href) -> [ContentLink(text, href, []), ..accum] + _ -> accum + } + + jot.Heading(attributes:, level: 3, content: [jot.Text(text)]) -> + case dict.get(attributes, "id"), accum { + Ok(href), [first, ..rest] -> [ + ContentLink( + ..first, + children: list.append(first.children, [ + ContentLink(text, href, []), + ]), + ), + ..rest + ] + _, _ -> accum + } + _ -> accum + } + }) + |> list.reverse +} + +pub fn base_layout( + page_content: List(Element(a)), + page: PageMeta, + ctx: site.Context, +) -> Element(a) { + html.html([], [ + html.head([], head_elements(page, ctx)), + html.body( + [], + list.append(page_content, [ + footer(ctx), + html.script([attr.src("/javascript/main.js"), attr("async", "")], ""), + ]), + ), + ]) +} + +pub fn layout_table_of_contents( + content: List(Element(a)), + table_of_contents: List(ContentLink), + meta: PageMeta, + ctx: site.Context, +) { + [ + header(hero_image: option.None, content: [ + html.h1([], [html.text(meta.title)]), + html.p([attr.class("hero-subtitle")], [html.text(meta.description)]), + ]), + html.main([attr.class("page toc-layout")], [ + html.nav([class("table-of-contents")], [ + html.h4([], [html.text("Table of Contents")]), + html.ul( + [], + list.map(table_of_contents, fn(item) { + html.li([], [ + html.a([attr.href(item.href)], [html.text(item.title)]), + case item.children { + [] -> element.none() + _ -> + html.ul( + [], + list.map(item.children, fn(item) { + html.li([], [ + html.a([attr.href(item.href)], [html.text(item.title)]), + ]) + }), + ) + }, + ]) + }), + ), + ]), + ..content + ]), + ] + |> base_layout(meta, ctx) +} + +pub fn layout_header( + content: List(Element(a)), + class: String, + meta: PageMeta, + ctx: site.Context, +) -> Element(a) { + [ + header(hero_image: option.None, content: [ + html.h1([], [html.text(meta.title)]), + html.p([attr.class("hero-subtitle")], [html.text(meta.subtitle)]), + ]), + html.main([attr.class("page content " <> class)], content), + ] + |> base_layout(meta, ctx) +} + +// Page elements + +fn head_elements(page: PageMeta, ctx: site.Context) -> List(element.Element(a)) { + let metatag = fn(property, content) { + html.meta([attr("property", property), attr("content", content)]) + } + let preview_image = case page.preview_image { + option.None -> ctx.hostname <> "/images/preview/site.png" + option.Some(name) -> ctx.hostname <> "/images/preview/" <> name <> ".png" + } + + [ + html.meta([attr("charset", "utf-8")]), + html.meta([attr("content", "width=device-width"), attr.name("viewport")]), + html.link([attr.href("/images/lucy/lucy.svg"), attr.rel("shortcut icon")]), + html.link([ + attr("title", "Gleam"), + attr.href(ctx.hostname <> "/feed.xml"), + attr.rel("alternate"), + attr.type_("application/atom+xml"), + ]), + html.title([], page.meta_title), + html.meta([attr("content", page.description), attr.name("description")]), + metatag("og:type", "website"), + metatag("og:image", preview_image), + metatag("og:title", page.meta_title), + metatag("og:description", page.description), + metatag("og:url", ctx.hostname <> "/" <> page.path), + metatag("twitter:card", "summary_large_image"), + metatag("twitter:url", ctx.hostname), + metatag("twitter:title", page.meta_title), + metatag("twitter:description", page.description), + metatag("twitter:image", preview_image), + html.script( + [ + attr.src("https://plausible.io/js/plausible.js"), + attr("data-domain", "gleam.run"), + attr("defer", ""), + attr("async", ""), + ], + "", + ), + html.link([ + attr.href("/styles/main.css?v=" <> ctx.styles_hash), + attr.rel("stylesheet"), + ]), + ..list.map(page.preload_images, fn(href) { + html.link([attr("as", "image"), attr.href(href), attr.rel("preload")]) + }) + ] +} + +pub fn header( + hero_image hero_image: Option(#(String, String)), + content content: List(Element(a)), +) -> Element(a) { + let hero_content = html.div([attr.class("text")], content) + let hero_content = case hero_image { + option.Some(#(src, alt)) -> [ + html.div( + [attr("data-show-pride", ""), class("hero-lucy-container wide-only")], + [html.img([attr.alt(alt), attr.src(src), attr.class("hero-lucy")])], + ), + html.div([class("text-left")], [hero_content]), + ] + option.None -> [hero_content] + } + + html.div([attr.class("page-header")], [ + html.nav([attr.class("navbar")], [ + html.div([attr.class("content")], [ + html.div([], [ + html.a([attr.href("/"), attr.class("logo")], [ + html.img([ + attr.alt("Lucy the star, Gleam's mascot"), + attr.src("/images/lucy/lucy.svg"), + attr.class("navbar-lucy"), + ]), + html.text("Gleam"), + ]), + ]), + html.div([], [ + html.a([attr.href("/news")], [html.text("News")]), + html.a([attr.href("/community")], [html.text("Community")]), + html.a([attr.href("/sponsor")], [html.text("Sponsor")]), + ]), + html.div([], [ + html.a([attr.href("https://packages.gleam.run")], [ + html.text("Packages"), + ]), + html.a([attr.href("/documentation")], [html.text("Docs")]), + html.a([attr.href("https://github.com/gleam-lang")], [ + html.text("Code"), + ]), + ]), + ]), + ]), + html.div([attr.class("hero")], [ + html.div([attr.class("content")], hero_content), + html.img([ + attr.alt("a soft wavey boundary between two sections of the website"), + attr.src("/images/waves.svg"), + attr.class("home-waves"), + ]), + ]), + ]) +} + +fn footer(ctx: site.Context) -> element.Element(a) { + let footer_links = [ + #("News", "/news"), + #("Cheat sheets", "/documentation#cheatsheets"), + #("Discord", "https://discord.gg/Fm8Pwmy"), + #("Code", "https://github.com/gleam-lang"), + #("Language tour", "https://tour.gleam.run"), + #("Playground", "https://playground.gleam.run"), + #("Documentation", "/documentation"), + #("Sponsor", "https://github.com/sponsors/lpil"), + #("Packages", "https://packages.gleam.run/"), + #("Gleam Weekly", "https://gleamweekly.com/"), + #("Roadmap", "/roadmap"), + #("Case studies", "/case-studies"), + ] + + let code_of_conduct = + "https://github.com/gleam-lang/gleam/blob/main/CODE_OF_CONDUCT.md" + + let #(date, _) = timestamp.to_calendar(ctx.time, calendar.utc_offset) + + html.footer([class("footer")], [ + html.div([class("content")], [ + html.div([class("first")], [ + html.a([attr.href("/"), class("logo")], [ + html.img([ + attr.alt("Lucy the star, Gleam's mascot"), + attr.src("/images/lucy/lucy.svg"), + class("footer-lucy"), + ]), + html.text("Gleam"), + ]), + ]), + html.ul( + [class("middle")], + list.map(footer_links, fn(pair) { + html.li([], [html.a([attr.href(pair.1)], [html.text(pair.0)])]) + }), + ), + html.ul([class("last")], [ + html.li([], [ + html.text("© " <> int.to_string(date.year) <> " Louis Pilfold"), + ]), + html.li([], [ + html.a([attr.href(code_of_conduct)], [html.text("Code of conduct")]), + ]), + ]), + ]), + ]) +} + type Sponsee { Sponsee(name: String, title: String, avatar: String, sponsor_link: String) } @@ -292,7 +560,7 @@ pub fn sponsor(ctx: site.Context) -> fs.File { ]), ..content ] - |> top_layout(meta, ctx) + |> base_layout(meta, ctx) |> to_html_file(meta) } @@ -385,7 +653,7 @@ pub fn case_study(post: case_study.CaseStudy, ctx: site.Context) -> fs.File { ]), ]), ] - |> page_layout("", meta, ctx) + |> layout_header("", meta, ctx) |> to_html_file(meta) } @@ -421,7 +689,7 @@ pub fn news_post(post: news.NewsPost, ctx: site.Context) -> fs.File { element.unsafe_raw_html("", "article", [class("prose")], post.content), ]), ] - |> page_layout("", meta, ctx) + |> layout_header("", meta, ctx) |> to_html_file(meta) } @@ -1047,7 +1315,7 @@ niceties.", html.text(" for an overview of the Gleam language."), ]), ] - |> page_layout("", meta, ctx) + |> layout_header("", meta, ctx) |> to_html_file(meta) } @@ -1063,7 +1331,14 @@ pub fn deployment_flyio(ctx: site.Context) -> fs.File { preview_image: option.None, ) - [ + let toc = [ + ContentLink("Prepare your application", "#prepare-your-application", []), + ContentLink("Add a Dockerfile", "#add-a-dockerfile", []), + ContentLink("Set up the Fly.io CLI", "#set-up-the-flyio-cli", []), + ContentLink("Deploy the application", "#deploy-the-application", []), + ] + + let content = [ html.p([], [ html.a([attr.href("https://fly.io")], [html.text("Fly.io")]), html.text( @@ -1195,7 +1470,13 @@ file. Once deployed you can open it in a web browser by running ", html.text(" after saving any changes to the source code."), ]), ] - |> page_layout("", meta, ctx) + + layout_table_of_contents( + [html.article([class("prose")], content)], + toc, + meta, + ctx, + ) |> to_html_file(meta) } @@ -1733,7 +2014,7 @@ version of Erlang on the computer used to compile the escript.", html.text(" to get help or share what you’re working on."), ]), ] - |> page_layout("prose", meta, ctx) + |> layout_header("prose", meta, ctx) |> to_html_file(meta) } @@ -2401,7 +2682,7 @@ workloads.", html.h2([attr.id("is-it-good")], [html.text("Is it good?")]), html.p([], [html.text("Yes, I think so. :)")]), ] - |> page_layout("prose", meta, ctx) + |> layout_header("prose", meta, ctx) |> to_html_file(meta) } @@ -2605,7 +2886,7 @@ pub fn documentation(ctx: site.Context) -> fs.File { ]), ]), ] - |> page_layout("", meta, ctx) + |> layout_header("", meta, ctx) |> to_html_file(meta) } @@ -2650,7 +2931,7 @@ pub fn news_index(posts: List(news.NewsPost), ctx: site.Context) -> fs.File { }) [html.ul([class("news-posts")], list_items)] - |> page_layout("", meta, ctx) + |> layout_header("", meta, ctx) |> to_html_file(meta) } @@ -2702,7 +2983,7 @@ pub fn case_studies_index( ]), ]), ] - |> page_layout("", meta, ctx) + |> layout_header("", meta, ctx) |> to_html_file(meta) } @@ -2863,7 +3144,7 @@ allow_write = [\"./database.sqlite\"]" ] content - |> page_layout("", meta, ctx) + |> layout_header("", meta, ctx) |> to_html_file(meta) } @@ -2879,7 +3160,43 @@ pub fn deployment_linux(ctx: site.Context) -> fs.File { preview_image: option.None, ) - [ + let toc = [ + ContentLink("Provision your server", "#provision-your-server", []), + ContentLink("Configure your DNS", "#configure-your-dns", []), + ContentLink("Prepare your application", "#prepare-your-application", []), + ContentLink("Add a Dockerfile", "#add-a-dockerfile", []), + ContentLink("Build your container on CI", "#build-your-container-on-ci", []), + ContentLink("Secure the SSH service", "#secure-the-ssh-service", []), + ContentLink( + "Secure the network with a firewall", + "#secure-the-network-with-a-firewall", + [], + ), + ContentLink( + "Enable automatic Ubuntu security updates", + "#enable-automatic-ubuntu-security-updates", + [], + ), + ContentLink("Install Caddy and Podman", "#install-caddy-and-podman", []), + ContentLink( + "Define your Podman container", + "#define-your-podman-container", + [], + ), + ContentLink("Start the container", "#start-the-container", []), + ContentLink( + "Configure Caddy to send traffic to the application", + "#configure-caddy-to-send-traffic-to-the-application", + [], + ), + ContentLink( + "Future deployments and maintenance", + "#future-deployments-and-maintenance", + [], + ), + ] + + let content = [ html.p([], [ html.text( "This guide will take you through the process of deploying a Gleam backend web @@ -3420,7 +3737,13 @@ systemctl restart webapp html.text("."), ]), ] - |> page_layout("", meta, ctx) + + layout_table_of_contents( + [html.article([class("prose")], content)], + toc, + meta, + ctx, + ) |> to_html_file(meta) } @@ -3526,7 +3849,7 @@ pub fn community(ctx: site.Context) -> fs.File { ] content - |> page_layout("", meta, ctx) + |> layout_header("", meta, ctx) |> to_html_file(meta) } @@ -3890,7 +4213,7 @@ ul { ] content - |> page_layout("", meta, ctx) + |> layout_header("", meta, ctx) |> to_html_file(meta) } @@ -3902,22 +4225,6 @@ pub fn short_human_date(date: calendar.Date) -> String { <> int.to_string(date.year) } -pub fn page_layout( - content: List(Element(a)), - class: String, - meta: PageMeta, - ctx: site.Context, -) -> Element(a) { - [ - header(hero_image: option.None, content: [ - html.h1([], [html.text(meta.title)]), - html.p([attr.class("hero-subtitle")], [html.text(meta.subtitle)]), - ]), - html.main([attr.class("page content " <> class)], content), - ] - |> top_layout(meta, ctx) -} - pub fn home(ctx: site.Context) -> fs.File { let meta = PageMeta( @@ -4248,66 +4555,10 @@ pub fn register_event_handler() { ] content - |> top_layout(meta, ctx) + |> base_layout(meta, ctx) |> to_html_file(meta) } -fn header( - hero_image hero_image: Option(#(String, String)), - content content: List(Element(a)), -) -> Element(a) { - let hero_content = html.div([attr.class("text")], content) - let hero_content = case hero_image { - option.Some(#(src, alt)) -> [ - html.div( - [attr("data-show-pride", ""), class("hero-lucy-container wide-only")], - [html.img([attr.alt(alt), attr.src(src), attr.class("hero-lucy")])], - ), - html.div([class("text-left")], [hero_content]), - ] - option.None -> [hero_content] - } - - html.div([attr.class("page-header")], [ - html.nav([attr.class("navbar")], [ - html.div([attr.class("content")], [ - html.div([], [ - html.a([attr.href("/"), attr.class("logo")], [ - html.img([ - attr.alt("Lucy the star, Gleam's mascot"), - attr.src("/images/lucy/lucy.svg"), - attr.class("navbar-lucy"), - ]), - html.text("Gleam"), - ]), - ]), - html.div([], [ - html.a([attr.href("/news")], [html.text("News")]), - html.a([attr.href("/community")], [html.text("Community")]), - html.a([attr.href("/sponsor")], [html.text("Sponsor")]), - ]), - html.div([], [ - html.a([attr.href("https://packages.gleam.run")], [ - html.text("Packages"), - ]), - html.a([attr.href("/documentation")], [html.text("Docs")]), - html.a([attr.href("https://github.com/gleam-lang")], [ - html.text("Code"), - ]), - ]), - ]), - ]), - html.div([attr.class("hero")], [ - html.div([attr.class("content")], hero_content), - html.img([ - attr.alt("a soft wavey boundary between two sections of the website"), - attr.src("/images/waves.svg"), - attr.class("home-waves"), - ]), - ]), - ]) -} - pub fn to_html_file(page_content: Element(a), meta: PageMeta) -> fs.File { fs.HtmlPage( path: meta.path, @@ -4315,124 +4566,6 @@ pub fn to_html_file(page_content: Element(a), meta: PageMeta) -> fs.File { ) } -fn top_layout( - page_content: List(Element(a)), - page: PageMeta, - ctx: site.Context, -) -> Element(a) { - html.html([], [ - html.head([], head_elements(page, ctx)), - html.body( - [], - list.append(page_content, [ - footer(ctx), - html.script([attr.src("/javascript/main.js"), attr("async", "")], ""), - ]), - ), - ]) -} - -fn head_elements(page: PageMeta, ctx: site.Context) -> List(element.Element(a)) { - let metatag = fn(property, content) { - html.meta([attr("property", property), attr("content", content)]) - } - let preview_image = case page.preview_image { - option.None -> ctx.hostname <> "/images/preview/site.png" - option.Some(name) -> ctx.hostname <> "/images/preview/" <> name <> ".png" - } - - [ - html.meta([attr("charset", "utf-8")]), - html.meta([attr("content", "width=device-width"), attr.name("viewport")]), - html.link([attr.href("/images/lucy/lucy.svg"), attr.rel("shortcut icon")]), - html.link([ - attr("title", "Gleam"), - attr.href(ctx.hostname <> "/feed.xml"), - attr.rel("alternate"), - attr.type_("application/atom+xml"), - ]), - html.title([], page.meta_title), - html.meta([attr("content", page.description), attr.name("description")]), - metatag("og:type", "website"), - metatag("og:image", preview_image), - metatag("og:title", page.meta_title), - metatag("og:description", page.description), - metatag("og:url", ctx.hostname <> "/" <> page.path), - metatag("twitter:card", "summary_large_image"), - metatag("twitter:url", ctx.hostname), - metatag("twitter:title", page.meta_title), - metatag("twitter:description", page.description), - metatag("twitter:image", preview_image), - html.script( - [ - attr.src("https://plausible.io/js/plausible.js"), - attr("data-domain", "gleam.run"), - attr("defer", ""), - attr("async", ""), - ], - "", - ), - html.link([ - attr.href("/styles/main.css?v=" <> ctx.styles_hash), - attr.rel("stylesheet"), - ]), - ..list.map(page.preload_images, fn(href) { - html.link([attr("as", "image"), attr.href(href), attr.rel("preload")]) - }) - ] -} - -fn footer(ctx: site.Context) -> element.Element(a) { - let footer_links = [ - #("News", "/news"), - #("Cheat sheets", "/documentation#cheatsheets"), - #("Discord", "https://discord.gg/Fm8Pwmy"), - #("Code", "https://github.com/gleam-lang"), - #("Language tour", "https://tour.gleam.run"), - #("Playground", "https://playground.gleam.run"), - #("Documentation", "/documentation"), - #("Sponsor", "https://github.com/sponsors/lpil"), - #("Packages", "https://packages.gleam.run/"), - #("Gleam Weekly", "https://gleamweekly.com/"), - #("Roadmap", "/roadmap"), - #("Case studies", "/case-studies"), - ] - - let code_of_conduct = - "https://github.com/gleam-lang/gleam/blob/main/CODE_OF_CONDUCT.md" - - let #(date, _) = timestamp.to_calendar(ctx.time, calendar.utc_offset) - - html.footer([class("footer")], [ - html.div([class("content")], [ - html.div([class("first")], [ - html.a([attr.href("/"), class("logo")], [ - html.img([ - attr.alt("Lucy the star, Gleam's mascot"), - attr.src("/images/lucy/lucy.svg"), - class("footer-lucy"), - ]), - html.text("Gleam"), - ]), - ]), - html.ul( - [class("middle")], - list.map(footer_links, fn(pair) { - html.li([], [html.a([attr.href(pair.1)], [html.text(pair.0)])]) - }), - ), - html.ul([class("last")], [ - html.li([], [ - html.text("© " <> int.to_string(date.year) <> " Louis Pilfold"), - ]), - html.li([], [ - html.a([attr.href(code_of_conduct)], [html.text("Code of conduct")]), - ]), - ]), - ]), - ]) -} - fn wall_of_sponsors() -> Element(a) { let sponsors = list.shuffle(sponsor.sponsors) diff --git a/src/website/roadmap.gleam b/src/website/roadmap.gleam index 13b572ce..09dc0b63 100644 --- a/src/website/roadmap.gleam +++ b/src/website/roadmap.gleam @@ -304,6 +304,6 @@ pub fn page(ctx: site.Context) -> fs.File { }), ), ] - |> page.page_layout("roadmap", meta, ctx) + |> page.layout_header("roadmap", meta, ctx) |> page.to_html_file(meta) }