From 72fb84a820d9657bef6e03094d0c0acbc2688ec9 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Wed, 15 Jan 2020 19:04:44 -0800 Subject: [PATCH 1/8] Add hacked proof-of-concept for "Missing content" message flash fix. --- .../extensible-markdown-parsing-in-elm.md | 189 ------------- examples/docs/content/blog/index.md | 4 - examples/docs/content/blog/static-http.md | 200 -------------- .../content/blog/types-over-conventions.md | 169 ------------ .../docs/content/docs/directory-structure.md | 79 ------ examples/docs/content/docs/index.md | 19 -- examples/docs/content/index.md | 73 ----- examples/docs/src/Main.elm | 7 +- src/Pages/ContentCache.elm | 256 ++++++++++++++++-- 9 files changed, 232 insertions(+), 764 deletions(-) delete mode 100644 examples/docs/content/blog/extensible-markdown-parsing-in-elm.md delete mode 100644 examples/docs/content/blog/index.md delete mode 100644 examples/docs/content/blog/static-http.md delete mode 100644 examples/docs/content/blog/types-over-conventions.md delete mode 100644 examples/docs/content/docs/directory-structure.md delete mode 100644 examples/docs/content/docs/index.md delete mode 100644 examples/docs/content/index.md diff --git a/examples/docs/content/blog/extensible-markdown-parsing-in-elm.md b/examples/docs/content/blog/extensible-markdown-parsing-in-elm.md deleted file mode 100644 index 8ba645fd7..000000000 --- a/examples/docs/content/blog/extensible-markdown-parsing-in-elm.md +++ /dev/null @@ -1,189 +0,0 @@ ---- -{ - "type": "blog", - "author": "Dillon Kearns", - "title": "Extensible Markdown Parsing in Pure Elm", - "description": "Introducing a new parser that extends your palette with no additional syntax", - "image": "/images/article-covers/extensible-markdown-parsing.jpg", - "published": "2019-10-08", -} ---- - -I'm excited to share a new approach to markdown parsing for the Elm ecosystem: [`dillonkearns/elm-markdown`](https://package.elm-lang.org/packages/dillonkearns/elm-markdown/latest/)! - -As a matter of fact, the blog post you're reading right now is being rendered with it. - -## Why does Elm need another markdown parser? - -I built this tool so that I could: - -- Render markdown blocks using my preferred UI library (`elm-ui`, in my case, but you could use `elm-css` or anything else!) -- Extend what can be expressed beyond the standard markdown blocks like headings, lists, etc. -- Inject data from my Elm model into my markdown - -And yet, I wanted to do all of this with the benefits that come from using standard Markdown: - -- Familiar syntax -- Great editor tooling (I write my blog posts in Ulysses using Markdown, and I have prettier set up to auto format markdown when I'm tweaking markdown directly in my code projects) -- Previews render in Github -- Easy for others to contribute (for example, to the [`elm-pages` docs](https://elm-pages.com/docs)) - -## Core Features - -So how do you get the best of both worlds? There are three key features that give `dillonkearns/elm-markdown` rich extensibility without actually adding to the Markdown syntax: - -- ⚙️ **Map HTML to custom Elm rendering functions** (for extensible markdown!) -- 🎨 **Use [custom renderers](https://package.elm-lang.org/packages/dillonkearns/elm-markdown/latest/Markdown-Parser#Renderer)** (for custom rendering with your preferred styles and UI library) -- 🌳 **Give users access to the parsed Markdown Blocks before rendering** (for inspecting, transforming, or extracting data from the parsed Markdown before passing it to your Markdown Renderer) - -Let's explore these three key features in more depth. - -## ⚙️ Map HTML to custom Elm rendering functions - -I didn't want to add additional features that weren't already a part of Markdown syntax. Since HTML is already valid Markdown, it seemed ideal to just use declarative HTML tags to express these custom view elements. `dillonkearns/elm-markdown` leverages that to give you a declarative Elm syntax to explicitly say what kind of HTML is accepted (think JSON Decoders) and, given that accepted HTML, how to render it. - -## Markdown Within HTML Tags - -What makes this especially useful is that we can render any Markdown content within our HTML tags. So you could have a Markdown file that looks like this. - -```markdown -## Markdown Within HTML (Within Markdown) - -You can now: - -- Render HTML within your Markdown -- Render Markdown within that HTML! - - - -Dillon really likes building things with Elm! - -Here are some links: - -- [Articles](https://incrementalelm.com/articles) - - -``` - -And here's the output: - - - -This is a nice way to abstract the presentation logic for team members' bios on an `about-us` page. We want richer presentation logic than plain markdown provides (for example, showing icons with the right dimensions, and displaying them in a row not column view, etc.) Also, since we're using Elm, we get pretty spoiled by explicit and precise error messages. So we'd like to get an error message if we don't provide a required attribute! - -Here's the relevant code for handling the `` HTML tag in our Markdown: - -```elm -Markdown.Html.oneOf - [ Markdown.Html.tag "bio" - (\name photoUrl twitter github dribbble renderedChildren -> - bioView renderedChildren name photoUrl twitter github dribbble - ) - |> Markdown.Html.withAttribute "name" - |> Markdown.Html.withAttribute "photo" - |> Markdown.Html.withOptionalAttribute "twitter" - |> Markdown.Html.withOptionalAttribute "github" - |> Markdown.Html.withOptionalAttribute "dribbble" - ] -``` - -If we forget to pass in the required `photo` attribute, we'll get an error message like this: - -``` -Problem with the given value: - - - -Expecting attribute "photo". -``` - -### Avoiding low-level HTML in markdown - -If you're familiar with [MDX](https://mdxjs.com) (it's Markdown syntax, but extended with some extra syntax from JSX, including like JS `import`s and JSX HTML tags). Guillermo Rauch, the creator of MDX even talks about the benefits that a more declarative approach, like the one `dillonkearns/elm-markdown` takes, could have over the current MDX approach of using low-level `import` statements and JSX syntax [in this talk (around 20:36 - 22:30)](https://www.youtube.com/watch?v=8oFJPVOT7FU&feature=youtu.be&t=1236). - -Even with this declarative approach to explicitly allowing the HTML tags you want, it's possible to get very low-level and just create mappings to standard HTML tags. I like to treat the HTML tags within these markdown documents like Web Components rather than raw HTML. That means using it as a very high-level way of expressing your custom views. With standard Github-flavored markdown, you'll often see people injecting `
` tags with styles, or `` tags, etc. I consider this too low-level to be injecting into Markdown in most cases. The Markdown document should be more declarative, concerned only with _what_ to render, not _how_ to render it. - -## 🎨 Use custom renderers - -Many Markdown libraries just give you the rendered HTML directly. With `dillonkearns/elm-markdown`, one of the main goals was to give you full control over presentation at the initial render (rather than needing to add CSS rules to apply to your rendered output). I personally like to use `elm-ui` whenever I can, so I wanted to use that directly not just for my navbar, but to style my rendered markdown blocks. - -Beyond just rendering directly to your preferred UI library, custom Renderers also open up a number of new potential uses. You can render your Markdown into `elm-ui` `Element`s, but you could also render it to any other Elm type. That could be data, or even functions. Why would you render a function? Well, that would allow you to inject dynamic data from your Elm model! - -Some other use cases that custom Renderers enable: - -- Regular old `Html` (using the [`defaultHtmlRenderer`](https://package.elm-lang.org/packages/dillonkearns/elm-markdown/latest/Markdown-Parser#defaultHtmlRenderer)) -- Render into [`elm-ui`](https://package.elm-lang.org/packages/mdgriffith/elm-ui/latest/) `Element`s -- Render into ANSI color codes for rich formatting in terminal output -- Render into plain text with all formatting stripped out (for search functionality) - -### Performing validations in Renderers - -Another goal with `dillonkearns/elm-markdown` is to allow early and precise feedback. One of my favorite uses of Custom Renderers is to catch dead links (or images). `elm-pages` will stop the production build when the Renderer fails. [Here's the relevant code](https://github.com/dillonkearns/elm-pages/blob/c76e96af497406fb9acf294acebbcb0c0e391197/examples/docs/src/MarkdownRenderer.elm#L90-L93) from elm-pages.com - -```elm -renderer : Markdown.Parser.Renderer (Element msg) -renderer = - { - link = - \link body -> - Pages.isValidRoute link.destination - |> Result.map - , -- rest of the Renderer definition - } -``` - -## 🌳 Give users access to the parsed Markdown Blocks before rendering - -Exposing the AST allows for a number of powerful use cases as well. And it does so without requiring you to dig into the internals. You just get access to a nice Elm custom type and you can do what you want with it before passing it on to your Custom Renderer. - -Here are some use cases that this feature enables: - -- Extract metadata before rendering, like building a table of contents data structure with proper links ([here's an Ellie demo of that!](https://ellie-app.com/6QtYW8pcCDna1)) -- Run a validation and turn it into an `Err`, for example, if there are multiple level 1 headings (having multiple `h1`s on a page causes accessibility problems) -- Transform the blocks by applying formatting rules, for example use a title casing function on all headings -- Transform the AST before rendering it, for example dropping each heading down one level (H1s become H2s, etc.) - -## The future of `dillonkearns/elm-markdown` - -I've been really enjoying using this in production for several weeks. But it certainly isn't fully handling all cases in Github-flavored markdown. - -I'm running all 1400 end-to-end test cases from the Marked.js test suite (which is what `elm-explorations/markdown` runs under the hood). And that test suite includes running through every example in the [Github-flavored markdown spec](https://github.github.com/gfm/). You can see nicely formatted markdown with all of the current failures [here](https://github.com/dillonkearns/elm-markdown/tree/master/test-results/failing/GFM). It includes all failures from the Marked.js test suite, organized by feature area. I'm working through handling more of these cases to make it more widely useful, but feel free to use it now with that caveat in mind. - -Pull requests are very welcome, I would love community contributions on this project! If you're interested in contributing, check out [the contributing guide in the Github repo](https://github.com/dillonkearns/elm-markdown/blob/master/CONTRIBUTING.md). - -### Fault-Tolerance Versus Helpful Errors - -That said, the goal is not to get to 100% compliance with the Github-Flavored Markdown Spec. Markdown has a goal of being Fault-Tolerant, meaning it will always try to "do the best it can" rather than giving an error message when something unexpected happens. That means there's no such thing as "invalid markdown." But there is most certainly **"markup that probably doesn't do what you expected."** For example - -``` -[My link](/home oh wait I forgot to close this link tag... -``` - -⚠️ This is technically **valid** Markdown! - -It "does the best it can" with the input and renders to a raw string rather than rendering a link. So this is an example that is squarely in the category of markup that **"probably doesn't do what you expected."** - -The goal of `dillonkearns/elm-markdown` is not fault-tolerance. It prioritizes **helpful error messages** over fault-tolerance. Sound familiar? There is a very similar difference in philosophy between JavaScript and Elm. - -So the rule of thumb for `dillonkearns/elm-markdown` is: - -- Follow the Github-Flavored Markdown Spec whenever it doesn't cover up feedback about something that "probably doesn't do what you expected" -- Otherwise, break with the Github-Flavored Markdown Spec and instead give a helpful error message - -You can follow along with the [current GFM Spec Compliance here](https://github.com/dillonkearns/elm-markdown#current-github-flavored-markdown-compliance). - -Thanks for reading! If you give this library a try, let me know what you think. I'd love to hear from you! - -You can keep the conversation going on the #elm-pages channel on [the Elm Slack](http://elmlang.herokuapp.com/), or on this Twitter thread 👇 - - diff --git a/examples/docs/content/blog/index.md b/examples/docs/content/blog/index.md deleted file mode 100644 index 858e95996..000000000 --- a/examples/docs/content/blog/index.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: elm-pages blog -type: blog-index ---- diff --git a/examples/docs/content/blog/static-http.md b/examples/docs/content/blog/static-http.md deleted file mode 100644 index 6a1b0fcdf..000000000 --- a/examples/docs/content/blog/static-http.md +++ /dev/null @@ -1,200 +0,0 @@ ---- -{ - "type": "blog", - "author": "Dillon Kearns", - "title": "A is for API - Introducing Static HTTP Requests", - "description": "", - "image": "/images/article-covers/static-http.jpg", - "published": "2019-12-10", -} ---- - -I'm excited to announce a new feature that brings `elm-pages` solidly into the JAMstack: Static HTTP requests. JAMstack stands for JavaScript, APIs, and Markup. And Static HTTP is all about pulling API data into your `elm-pages` site. - -If you’ve tried `elm-pages`, you may be thinking, "elm-pages hydrates into a full Elm app... so couldn’t you already make HTTP requests to fetch API data, like you would in any Elm app?" Very astute observation! You absolutely could. - -So what's new? It all comes down to these key points: - -* Less boilerplate -* Improved reliability -* Better performance - -Let's dive into these points in more detail. - -## Less boilerplate - -Let's break down how you perform HTTP requests in vanilla Elm, and compare that to how you perform a Static HTTP request with `elm-pages`. - -### Anatomy of HTTP Requests in Vanilla Elm -* Cmd for an HTTP request on init (or update) -* You receive a `Msg` in `update` with the payload -* Store the data in `Model` -* Tell Elm how to handle `Http.Error`s (including JSON decoding failures) - -### Anatomy of Static HTTP Requests in `elm-pages` -* `view` function specifies some `StaticHttp` data, and a function to turn that data into your `view` and `head` tags for that page - -That's actually all of the boilerplate for `StaticHttp` requests! - -There is a lifecycle, because things can still fail. But the entire Static HTTP lifecycle happens *before your users have even requested a page*. The requests are performed at build-time, and that means less boilerplate for you to maintain in your Elm code! - - -### Let's see some code! -Here's a code snippet for making a StaticHttp request. This code makes an HTTP request to the Github API to grab the current number of stars for the `elm-pages` repo. - -```elm -import Pages.StaticHttp as StaticHttp -import Pages -import Head -import Secrets -import Json.Decode.Exploration as Decode - - -view : - { path : PagePath Pages.PathKey - , frontmatter : Metadata - } - -> - StaticHttp.Request - { view : Model -> - View -> { title : String, body : Html Msg } - , head : List (Head.Tag Pages.PathKey) - } -view page = - (StaticHttp.get - (Secrets.succeed - "https://api.github.com/repos/dillonkearns/elm-pages") - (Decode.field "stargazers_count" Decode.int) - ) - |> StaticHttp.map - (\starCount -> - { view = - \model renderedMarkdown -> - { title = "Landing Page" - , body = - [ header starCount - , pageView model renderedMarkdown - ] - } - , head = head starCount - } - ) - - -head : Int -> List (Head.Tag Pages.PathKey) -head starCount = - Seo.summaryLarge - { canonicalUrlOverride = Nothing - , siteName = "elm-pages - " - ++ String.fromInt starCount - ++ " GitHub Stars" - , image = - { url = images.iconPng - , alt = "elm-pages logo" - , dimensions = Nothing - , mimeType = Nothing - } - , description = siteTagline - , locale = Nothing - , title = "External Data Example" - } - |> Seo.website -``` - -The data is baked into our built code, which means that the star count will only update when we trigger a new build. This is a common JAMstack technique. Many sites will trigger builds periodically to refresh data. Or better yet, use a webhook to trigger new builds whenever new data is available (for example, if you add a new blog post or a new page using a service like Contentful). - -Notice that this app's `Msg`, `Model`, and `update` function are not involved in the process at all! It's also worth noting that we are passing that data into our `head` function, which allows us to use it in our `` tags for the page. - -The `StaticHttp` functions are very similar to Elm libraries -you've likely used already, such as `elm/json` or `elm/random`. -If you don't depend on any StaticHttp data, you use `StaticHttp.succeed`, -similar to how you might use `Json.Decode.succeed`, `Random.constant`, -etc. - - -```elm -import Pages.StaticHttp as StaticHttp - - -StaticHttp.succeed - { view = - \model renderedMarkdown -> - { title = "Landing Page" - , body = - [ header - , pageView model renderedMarkdown - ] - } - , head = head - } -``` - -This is actually the same as our previous example that had a `StaticHttp.request`, except that it doesn't make a request or have the -stargazer count data. - -### Secure Secrets -A common pattern is to use environment variables in your local environment or your CI environment in order to securely manage -auth tokens and other secure data. `elm-pages` provides an API for accessing this data directly from your environment variables. -You don't need to wire through any flags or ports, simply use the [`Pages.Secrets` module (see the docs for more info)](https://package.elm-lang.org/packages/dillonkearns/elm-pages/latest/Pages-Secrets). It will take care of masking the secret data for you -so that it won't be accessible in the bundled assets (it's just used to perform the requests during the build step, and then -it's masked in the production assets). - -### The Static HTTP Lifecycle -If you have a bad auth token in your URL, or your JSON decoder fails, then that code will never run for your `elm-pages` site. Instead, you'll get a friendly `elm-pages` build-time error telling you exactly what the problem was and where it occurred (as you're familiar with in Elm). - -![StaticHttp build error](/images/static-http-error.png) - -These error messages are inspired by Elm's famously helpful errors. They're designed to point you in the right direction, and provide as much context as possible. - -Which brings us to our next key point... - -## Improved reliability -Static HTTP requests are performed at build-time. Which means that if you have a problem with one of your Static HTTP requests, *your users will never see it*. Even if a JSON decoder fails, `elm-pages` will report back the decode failure and wait until its fixed before it allows you to create your production build. - -Your API might go down, but your Static HTTP requests will always be up (assuming your site is up). The responses from your Static requests are baked into the static files for your `elm-pages` build. If there is an API outage, you of course won't be able to rebuild your site with fresh data from that API. But you can be confident that, though your build may break, your site will always have a working set of Static HTTP data. - -Compare this to an HTTP request in a vanilla Elm app. Elm can guarantee that you've handled all error cases. But you still need to handle the case where you have a bad HTTP response, or a JSON decoder fails. That's the best that Elm can do because it can't guarantee anything about the data you'll receive at runtime. But `elm-pages` *can* make guarantees about the data you'll receive! Because it introduces a new concept of data that you get a snapshot of during your build step. `elm-pages` guarantees that this frozen moment of time has no problems before the build succeeds, so we can make even stronger guarantees than we can with plain Elm. - -## Better performance -The StaticHttp API also comes with some significant performance boosts. StaticHttp data is just a static JSON file for each page in your `elm-pages` site. That means that: - -* No waiting on database queries to fetch API data -* Your site, including API responses, is just static files so it can be served through a blazing-fast CDN (which serves files from the nearest server in the user's region) -* Scaling is cheap and doesn't require an Ops team -* `elm-pages` intelligently prefetches the Static HTTP data for a page when you're likely to navigate to that page, so page loads are instant and there's no spinner waiting to load that initial data -* `elm-pages` optimizes your `StaticHttp` JSON data, stripping out everything but what you use in your JSON decoder - -### JSON Optimization -The JSON optimization is made possible by a JSON parsing library created by Ilias Van Peer. Here's the pull request where he introduced the JSON optimization functionality: [github.com/zwilias/json-decode-exploration/pull/9](https://github.com/zwilias/json-decode-exploration/pull/9). - -Let's take our Github API request as an example. Our Github API request from our previous code snippet ([https://api.github.com/repos/dillonkearns/elm-pages](https://api.github.com/repos/dillonkearns/elm-pages)) has a payload of 5.6KB (2.4KB gzipped). That size of the optimized JSON drops down to about 3% of that. - -You can inspect the network tab on this page and you'll see something like this: - -![StaticHttp content request](/images/static-http-content-requests.png) - -If you click on Github API link above and compare it, you'll see that it's quite a bit smaller! It just has the one field that we grabbed in our JSON decoder. - -This is quite nice for privacy and security purposes as well because any personally identifying information that might be included in an API response you consume won't show up in your production bundle (unless you were to explicitly include it in a JSON decoder). - -### Comparing StaticHttp to other JAMstack data source strategies -You may be familiar with frameworks like Gatsby or Gridsome which also allow you to build data from external sources into your static site. Those frameworks, however, use a completely different approach, [using a GraphQL layer to store data from those data sources](https://www.gatsbyjs.org/docs/page-query/), and then looking that data up in GraphQL queries from within your static pages. - - -This approach makes sense for those frameworks. But since `elm-pages` is built on top of a language that already has an excellent type system, I wanted to remove that additional layer of abstraction and provide a simpler way to consume static data. The fact that Elm functions are all deterministic (i.e. given the same inputs they will always have the same outputs) opens up exciting new approaches to these problems as well. One of Gatsby's stated reasons for encouraging the use of their GraphQL layer is that it allows you to have your data all in one place. But the `elm-pages` StaticHttp API gives you similar benefits, using familiar Elm techniques like `map`, `andThen`, etc to massage your data into the desired format. - -## Future plans - -I'm looking forward to exploring more possibilities for using static data in `elm-pages`. Some things I plan to explore are: - -* Programatically creating pages using the Static HTTP API -* Configurable image optimization (including producing multiple dimensions for `srcset`s) using a similar API -* Optimizing the page metadata that is included for each page (i.e. code splitting) by explicitly specifying what metadata the page depends on using an API similar to StaticHttp - -## Getting started with StaticHttp - -You can [take a look at this an end-to-end example app that uses the new `StaticHttp` library](https://github.com/dillonkearns/elm-pages/blob/master/examples/external-data/src/Main.elm) to get started. - -Or just use the [`elm-pages-starter` repo](https://github.com/dillonkearns/elm-pages-starter) and start building something cool! Let me know your thoughts on Slack, I'd love to hear from you! Or continue the conversation on Twitter! - - \ No newline at end of file diff --git a/examples/docs/content/blog/types-over-conventions.md b/examples/docs/content/blog/types-over-conventions.md deleted file mode 100644 index 3789eeec1..000000000 --- a/examples/docs/content/blog/types-over-conventions.md +++ /dev/null @@ -1,169 +0,0 @@ ---- -{ - "type": "blog", - "author": "Dillon Kearns", - "title": "Types Over Conventions", - "description": "How elm-pages approaches configuration, using type-safe Elm.", - "image": "/images/article-covers/introducing-elm-pages.jpg", - "draft": true, - "published": "2019-09-21", -} ---- - -Rails started a movement of simplifying project setup with [a philosophy of "Convention Over Configuration"](https://rubyonrails.org/doctrine/#convention-over-configuration). This made for a very easy experience bootstrapping a new web server. The downside is that you have a lot of implicit rules that can be hard to follow. - -`elm-pages` takes a different approach. Rather than implicit conventions, or verbose configuration, `elm-pages` is centered around letting you explicitly configure your project using Elm's type system. This makes it a lot easier to configure because the Elm compiler will give you feedback on what the valid options are. And it also gives you the ability to define your own defaults and conventions explicitly, giving you the confidence getting started that Rails gives you, but the explicitness and helpful compiler support we're accustomed to in Elm. - -**Note:** `elm-pages` currently relies on a few basic conventions such as the name of the `content` folder which has your markup. Convention over configuration isn't evil. It just has a set of tradeoffs, like any other design. `elm-pages` shares the Elm philosophy's idea that ["There are worse things than being explicit"](https://twitter.com/czaplic/status/928359289135046656). In other words, implicit rules that are hard to trace is more likely to cause maintainability issues than a little extra typing to explicitly lay out some core rules. (As long as that extra typing is nice, type-safe Elm code!) - -Consider how `elm-pages` handles choosing a template for your pages. Many static site generators use [a special framework-provided frontmatter directive](https://jekyllrb.com/docs/front-matter/#predefined-global-variables) that determines which layout to use. And a special file naming convention will be used as the fallback for the default layout if you don't specify a layout in the frontmatter. - -With `elm-pages`, there are no magic frontmatter directives. The way you define and handle your metadata is completely up to you. `elm-pages` simply hands you the metadata types you define and allows you to choose how to handle them with the Elm compiler there to support you. - -## Let's see the code! - -If we wanted to define a particular layout for blog posts, and a different layout for podcast episodes, then it's as simple as defining a JSON decoder for the data in our frontmatter. - -So here's the frontmatter for a blog post: - -```markdown ---- -author: dillon -title: Types Over Conventions -published: 2019-09-21 ---- -``` - -And here's the frontmatter for a regular page: - -```markdown ---- -title: About elm-pages ---- -``` - -As far as `elm-pages` is concerned, this is just data. We define the rules for what to do with those different data types in our code. - -```elm -import Author -import Json.Decode - -type Metadata - = Page { title : String } - | BlogPost { author : String, title : String } - - -document = - Pages.Document.parser - { extension = "md" - , metadata = - Json.Decode.oneOf - [ - Json.Decode.map - (\title -> Page { title = title }) - (Json.Decode.field "title" Json.Decode.string) - , Json.Decode.map2 - (\author title -> - BlogPost { author = author, title = title } - ) - (Json.Decode.field "author" Author.decoder) - (Json.Decode.field "title" Json.Decode.string) - ] - , body = markdownView - } - -markdownView : String -> Result String (List (Html Msg)) -markdownView markdownBody = - MarkdownRenderer.view markdownBody -``` - -Each file in the `content` folder will result in a new route for your static site. You can define how to render the types of document in the `content` folder based on the extension any way you like. - -Now, in our `elm-pages` app, our `view` function will get the markdown that we rendered for a given page along with the corresponding `Metadata`. It's completely in our hands what we want to do with that data. - -Rails started a movement of simplifying project setup with [a philosophy of "Convention Over Configuration"](https://rubyonrails.org/doctrine/#convention-over-configuration). This made for a very easy experience bootstrapping a new web server. The downside is that you have a lot of implicit rules that can be hard to follow. - -`elm-pages` gives you the best of both worlds. Rather than implicit conventions, or verbose configuration, `elm-pages` is centered around letting you explicitly configure your project using Elm's type system. This makes it a lot easier to configure because the Elm compiler will give you feedback on what the valid options are. And it also gives you the ability to define your own defaults and conventions explicitly, giving you the simplicity of the Rails philosophy, but the explicitness and helpful compiler support we're accustomed to in Elm. - -**Note:** `elm-pages` currently relies on a few basic conventions such as the name of the `content` folder which has your markup. Convention over configuration isn't evil. It just has a set of tradeoffs, like any other design. `elm-pages` shares the Elm philosophy's idea that ["There are worse things than being explicit"](https://twitter.com/czaplic/status/928359289135046656). In other words, implicit rules that are hard to trace is more likely to cause maintainability issues than a little extra typing to explicitly lay out some core rules. As long as that extra typing is nice, type-safe Elm code! - - - -Consider how `elm-pages` handles choosing a template for your pages. Many static site generators use [a special framework-provided frontmatter directive](https://jekyllrb.com/docs/front-matter/#predefined-global-variables) that determines which layout to use. And a special file naming convention will be used as the fallback for the default layout if you don't specify a layout in the frontmatter. - -With `elm-pages`, there are no magic frontmatter directives. The way you define and handle your metadata is completely up to you. `elm-pages` simply hands you the metadata types you define and allows you to choose how to handle them with the Elm compiler there to support you. - -## Let's see the code! - -If we wanted to define a particular layout for blog posts, and a different layout for regular pages, then it's as simple as defining a JSON decoder for the data in our frontmatter. - -So here's the frontmatter for a blog post: - -```markdown ---- -author: dillon -title: Types Over Conventions -published: 2019-09-21 ---- -``` - -And here's the frontmatter for a regular page: - -```markdown ---- -title: About elm-pages ---- -``` - -As far as `elm-pages` is concerned, this is just data. We define the rules for what to do with those different data types in our code. - -Here's how we set up a parser to handle the frontmatter and body of our `.md` files in our `content` folder. -The raw frontmatter can be a variety of formats, including YAML, TOML, and JSON. But in our Elm code, -we turn that data into the data representing our app's metadata using a `Json.Decoder`. - -```elm -import Author --- Author is our custom module that looks --- up author data from their first name -import Json.Decode - -type Metadata - = Page { title : String } - | BlogPost { author : String, title : String } - -document = - Pages.Document.parser - { extension = "md" - , metadata = - Json.Decode.oneOf - [ Json.Decode.map - (\title -> - Page { title = title } - ) - (Json.Decode.field "title" Json.Decode.string) - , Json.Decode.map2 - (\author title -> - BlogPost { author = author, title = title } - ) - (Json.Decode.field "author" Author.decoder) - (Json.Decode.field "title" Json.Decode.string) - ] - , body = markdownView - } - - -markdownView : String -> Result String (List (Html Msg)) -markdownView markdownBody = - MarkdownRenderer.view markdownBody -``` - -Each file in the `content` folder will result in a new route for your static site. You can define how to render the types of document in the `content` folder based on the extension any way you like. - -Now, in our `elm-pages` app, our `view` function will get the markdown that we rendered for a given page along with the corresponding `Metadata`. It's completely in our hands what we want to do with that data. - -## Takeaways -So which is better, configuration through types or configuration by convention? - -They both have their benefits. If you're like me, then you enjoy being able to figure out what your Elm code is doing by just following the types. And I hope you'll agree that `elm-pages` gives you that experience for wiring up your content and your parsers. - -And when you need to do something more advanced, you've got all the typed data right there and you're empowered to solve the problem using Elm! \ No newline at end of file diff --git a/examples/docs/content/docs/directory-structure.md b/examples/docs/content/docs/directory-structure.md deleted file mode 100644 index e19b455b4..000000000 --- a/examples/docs/content/docs/directory-structure.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -title: Directory Structure -type: doc ---- - -## Philosophy - -As a general rule, `elm-pages` strives to be unopinionated about how you organize -your files (both code and content). - -```shell -. -├── content/ -├── elm.json -├── images/ -├── static/ -├── index.js -├── package.json -└── src/ - └── Main.elm -``` - -## `content` folder - -Each file in the `content` folder will result in a new route for your static site. You can define how to render the types of document in the `content` folder based on the extension any way you like. - -```elm -helloDocument : Pages.Document.DocumentParser Metadata (List (Html Msg)) -helloDocument = - Pages.Document.parser - { extension = "txt" - , metadata = - -- pages will use the layout for Docs if they have - -- `type: doc` in their markdown frontmatter - Json.Decode.map2 - (\title maybeType -> - case maybeType of - Just "doc" -> - Metadata.Doc { title = title } - - _ -> - Metadata.Page { title = title } - ) - (Json.Decode.field "title" Json.Decode.string) - (Json.Decode.field "type" Json.Decode.string - |> Json.Decode.maybe - ) - , body = MarkdownRenderer.view - } - -``` - -```elm -markdownDocument : Pages.Document.DocumentParser Metadata (List (Element Msg)) -markdownDocument = - Pages.Document.parser - { extension = "md" - , metadata = - Json.Decode.map2 - (\title maybeType -> - case maybeType of - Just "doc" -> - Metadata.Doc { title = title } - - _ -> - Metadata.Page { title = title } - ) - (Json.Decode.field "title" Json.Decode.string) - (Json.Decode.field "type" Json.Decode.string - |> Json.Decode.maybe - ) - , body = MarkdownRenderer.view - } - -``` - -## Metadata - -You define how your metadata is parsed diff --git a/examples/docs/content/docs/index.md b/examples/docs/content/docs/index.md deleted file mode 100644 index 5b38488f6..000000000 --- a/examples/docs/content/docs/index.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: Quick Start -type: doc ---- - -## Installing - -The easiest way to get set up is to use the starter template. Just go to the [`elm-pages-starter` repo](https://github.com/dillonkearns/elm-pages-starter) and click "Use this template" to fork the repo. - -Or clone down the repo: - -``` -git clone git@github.com:dillonkearns/elm-pages-starter.git -cd elm-pages-starter -npm install -npm start # starts a local dev server using `elm-pages develop` -``` - -From there, start editing the posts in the `content` folder. You can change the types of content in `src/Metadata.elm`, or render your content using a different renderer (the template uses `elm-explorations/markdown`) by changing [the configuring the document handlers](https://github.com/dillonkearns/elm-pages-starter/blob/2c2241c177cf8e0144af4a8afec0115f93169ac5/src/Main.elm#L70-L80). diff --git a/examples/docs/content/index.md b/examples/docs/content/index.md deleted file mode 100644 index 4004f34a3..000000000 --- a/examples/docs/content/index.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -title: elm-pages - a statically typed site generator -type: page ---- - -A **statically typed** site generator - - - -### Pure Elm Configuration - -Layouts, styles, even a full-fledged elm application. - -### 📄 Type-Safe Content - -Configuration, errors for broken links - - - -### 🚀 `elm-pages build` - -No `elm make` or `webpack` setup needed! Just one simple command. - - - - -### 📦 Optimized Elm Progressive Web App - -Layouts, styles, even a full-fledged elm application. - -### Deploy anywhere - -Ship to Netlify, Github Pages, or any host that will serve up static files! - - - - - -# No magic, just types - -The magic is in how the pieces snap together. The basic platform provided is simple, letting you compose exactly what you need with types to support you. - - - -# Extensible through pure elm - -Behavior is shared through packages exposing simple helper functions to help you build up your data. - - - -# If it compiles, it works - -`elm-pages` just makes more of the things you do in your static site feel like elm. Did you misspell the name of an image asset or a link to a blog post? `elm-pages` will give you a friendly error message and some helpful suggestions. - - - -# An extended elm platform - -`elm-pages` is just elm, but with a broader set of primitives for declaring meta tags to improve SEO, or generate RSS feeds and other files based on your static content. - - - -# Blazing fast - -All you have to do is create your content and choose how to present it. Optimized images, pre-rendered pages, and a snappy lightweight single-page app all come for free. - - - -# Simple - -`elm-pages` gives you the smallest set of core concepts possible, and a type system to make sure all the pieces fit together just right. The rest is up to you. Rather than remember a lot of magic and special cases, you can just rely on your elm types to build what you need with a set of simple primitives. - - diff --git a/examples/docs/src/Main.elm b/examples/docs/src/Main.elm index 1848b3525..6a0b756a4 100644 --- a/examples/docs/src/Main.elm +++ b/examples/docs/src/Main.elm @@ -42,7 +42,7 @@ manifest = , iarcRatingId = Nothing , name = "elm-pages docs" , themeColor = Just Color.white - , startUrl = pages.index + , startUrl = pages.blog.introducingElmPages , shortName = Just "elm-pages" , sourceIcon = images.iconPng } @@ -338,8 +338,9 @@ header stars currentPath = , Element.row [ Element.spacing 15 ] [ elmDocsLink , githubRepoLink stars - , highlightableLink currentPath pages.docs.directory "Docs" - , highlightableLink currentPath pages.blog.directory "Blog" + + --, highlightableLink currentPath pages.docs.directory "Docs" + --, highlightableLink currentPath pages.blog.directory "Blog" ] ] ] diff --git a/src/Pages/ContentCache.elm b/src/Pages/ContentCache.elm index cfced1612..6dcc11268 100644 --- a/src/Pages/ContentCache.elm +++ b/src/Pages/ContentCache.elm @@ -128,6 +128,31 @@ init document content = tuple ) |> combineTupleResults + --|> Result.map + -- (\soFar -> + -- soFar + -- |> List.map + -- (\( path, entry ) -> + -- let + -- initialThing = + -- case entry of + -- NeedContent string metadata -> + -- + -- + -- Unparsed string metadata contentJson -> + -- + -- + -- Parsed metadata contentJson -> + -- + -- --Parsed metadata + -- -- { body = renderer rawContent.body + -- -- , staticData = rawContent.staticData + -- -- } + -- -- |> Just + -- in + -- ( path, entry ) + -- ) + -- ) -- |> Result.mapError Dict.fromList |> Result.map Dict.fromList @@ -155,33 +180,208 @@ parseMetadata : parseMetadata document content = content |> List.map - (Tuple.mapSecond - (\{ frontMatter, extension, body } -> - let - maybeDocumentEntry = - Document.get extension document - in - case maybeDocumentEntry of - Just documentEntry -> - frontMatter - |> documentEntry.frontmatterParser - |> Result.map - (\metadata -> - -- TODO do I need to handle this case? - -- case body of - -- Just presentBody -> - -- Parsed metadata - -- { body = parseContent extension presentBody document - -- , staticData = "" - -- } - -- - -- Nothing -> - NeedContent extension metadata - ) + (\( path, { frontMatter, extension, body } ) -> + let + maybeDocumentEntry = + Document.get extension document + in + case maybeDocumentEntry of + Just documentEntry -> + frontMatter + |> documentEntry.frontmatterParser + |> Result.map + (\metadata -> + let + renderer = + \value -> + parseContent extension value document - Nothing -> - Err ("Could not find extension '" ++ extension ++ "'") - ) + thing = + Parsed metadata + { body = renderer """ + +After a round of closed beta testing (thank you to [Brian](https://twitter.com/brianhicks) and the [`elm-conf 2019`](https://2019.elm-conf.com/) organizing team!), I'm excited to share a new static site generator for Elm! + +[Matthew Griffith](https://twitter.com/mech_elephant) and I have had a lot of design discussions and sending code snippets back-and-forth to get to the current design. A big thank you to Matthew for the great discussions and, as always, his ability to look at the bigger picture and question basic assumptions to come up with awesome innovations! + +## What is `elm-pages` exactly? + +Well, this site you're looking at _right now_ is built with `elm-pages`! For example, the raw content for this post is from [`content/blog/introducing-elm-pages.md`](https://github.com/dillonkearns/elm-pages/blob/master/examples/docs/content/blog/introducing-elm-pages.md). + +`elm-pages` takes your static content and turns it into a modern, performant, single-page app. You can do anything you can with a regular Elm site, and yet the framework does a lot for you to optimize site performance and minimize tedious work. + +I see a lot of "roll your own" Elm static sites out there these days. When you roll your own Elm static site, you often: + +- Manage Strings for each page's content (rather than just having a file for each page) +- Wire up the routing for each page manually (or with a hand-made script) +- Add `` tags for SEO and to make Twitter/Slack/etc. link shares display the right image and title (or just skip it because it's a pain) + +I hope that `elm-pages` will make people's lives easier (and their load times faster). But `elm-pages` is for more than just building your blog or portfolio site. There's a movement now called JAMstack (JavaScript, APIs, and Markup) that is solving a broader set of problems with static sites. JAMstack apps do this by pulling in data from external sources, and using modern frontend frameworks to render the content (which then rehydrate into interactive apps). The goal is to move as much work as possible away from the user's browser and into a build step before pushing static files to your CDN host (but without sacrificing functionality). More and more sites are seeing that optimizing performance improves conversion rates and user engagement, and it can also make apps simpler to maintain. + +This is just the first release of `elm-pages`, but I've built a prototype for pulling in external data and am refining the design in preparation for the next release. Once that ships, the use cases `elm-pages` can handle will expand to things like ecommerce sites, job boards, and sites with content written by non-technical content editors. You can find a very informative FAQ and resources page about these ideas at [jamstack.org](https://jamstack.org/) (plus a more in-depth definition of the term JAMstack). + +## Comparing `elm-pages` and `elmstatic` + +`elm-pages` and [`elmstatic`](https://korban.net/elm/elmstatic/) have a lot of differences. At the core, they have two different goals. `elmstatic` generates HTML for you that doesn't include an Elm runtime. It uses Elm as a templating engine to do page layouts. It also makes some assumptions about the structure of your page content, separating `posts` and `pages` and automatically generating post indexes based on the top-level directories within the `posts` folder. It's heavily inspired by traditional static site generators like Jekyll. + +`elm-pages` hydrates into a single-page app that includes a full Elm runtime, meaning that you can have whatever client-side interactivity you want. It supports similar use cases to static site generators like [Gatsby](http://gatsbyjs.org). `elm-pages` makes a lot of optimizations by splitting and lazy-loading pages, optimizing image assets, and using service workers for repeat visits. It pre-renders HTML for fast first renders, but because it ships with JavaScript code it is also able to do some performance optimizations to make page changes faster (and without page flashes). So keep in mind that shipping without JavaScript doesn't necessarily mean your site performance suffers! You may have good reasons to want a static site with no JavaScript, but open up a Lighthouse audit and try it out for yourself rather than speculating about performance! + +Either framework might be the right fit depending on your goals. I hope this helps illuminate the differences! + +## How does `elm-pages` work? + +The flow is something like this: + +- Put your static content in your `content` folder (it could be Markdown, `elm-markup`, or something else entirely) +- Register Elm functions that define what to do with the [frontmatter](https://jekyllrb.com/docs/front-matter/) (that YAML data at the top of your markup files) and the body of each type of file you want to handle +- Define your app's configuration in pure Elm (just like a regular Elm `Browser.application` but with a few extra functions for SEO and site configuration) +- Run `elm-pages build` and ship your static files (JS, HTML, etc.) to Netlify, Github Pages, or your CDN of choice! + +The result is a blazing fast static site that is optimized both for the first load experience, and also uses some caching strategies to improve site performance for repeat visitors. You can look in your dev tools or run a Lighthouse audit on this page to see some of the performance optimizations `elm-pages` does for you! + +The way you set up an `elm-pages` app will look familiar if you have some experience with wiring up standard Elm boilerplate: + +```elm +main : Pages.Platform.Program Model Msg Metadata (List (Element Msg)) +main = + Pages.application + { init = init + , view = view + , update = update + , subscriptions = subscriptions + , documents = [ markdownHandler ] + , head = head + , manifest = manifest + , canonicalSiteUrl = "https://elm-pages.com" + } +``` + +You can take a look at [the `Main.elm` file for this site](https://github.com/dillonkearns/elm-pages/blob/master/examples/docs/src/Main.elm#L52) to get a better sense of the bigger picture. I'll do a more in-depth explanation of this setup in a future post. The short version is that + +`init`, `update`, and `subscriptions` are as you would expect (but maybe a bit simpler since `elm-pages` manages things like the URL for you). + +`documents` are where you define how to handle the frontmatter and body of the files in your `content` folder. And the `view` function gives you the result from your frontmatter and body, as well as your `Model`. + +`head` is just a function that passes you the metadata for a given page and lets you define tags to put in the `` (mostly for SEO). + +`manifest` lets you configure some settings that allow your app to be installed for offline use. + +And the end result is that `elm-pages` gets everything it needs about your site in order to optimize it and turn it into a modern, performant site that will get a great Lighthouse audit score! The goal is to make following best practices for a modern, performant static site one of the following: + +- Built-in +- Enforced by the Elm compiler +- Or at the very least the path of least resistence + +## What makes Elm awesome for building static sites + +JAMstack frameworks, like [Gatsby](http://gatsbyjs.org), can make powerful optimizations because they are dealing with strong constraints (specifically, content that is known at build time). Elm is the perfect tool for the JAMstack because it can leverage those constraints and turn them into compiler guarantees. Not only can we do more with static guarantees using Elm, but we can get additional guarantees using Elm's type-system and managed side-effects. It's a virtuous cycle that enables a lot of innovation. + +## Why use `elm-pages`? + +Let's take a look at a few of the features that make `elm-pages` worthwhile for the users (both the end users, and the team using it to build their site). + +### Performance + +- Pre-rendered pages for blazing fast first renders and improved SEO +- Your content is loaded as a single-page app behind the scenes, giving you smooth page changes +- Split individual page content and lazy load each page +- Prefetch page content on link hover so page changes are almost instant +- Image assets are optimized +- App skeleton is cached with a service worker (with zero configuration) so it's available offline + +One of the early beta sites that used `elm-pages` instantly shaved off over a megabyte for the images on a single page! Optimizations like that need to be built-in and automatic otherwise some things inevitably slip through the cracks. + +### Type-safety and simplicity + +- The type system guarantees that you use valid images and routes in the right places +- You can even set up a validation to give build errors if there are any broken links or images in your markdown +- You can set up validations to define your own custom rules for your domain! (Maximum title length, tag name from a set to avoid multiple tags with different wording, etc.) + +## Progressive Web Apps + +[Lighthouse recommends having a Web Manifest file](https://developers.google.com/web/tools/lighthouse/audits/manifest-exists) for your app to allow users to install the app to your home screen and have an appropriate icon, app name, etc. +Elm pages gives you a type-safe way to define a web manifest for your app: + +```elm +manifest : Manifest.Config PagesNew.PathKey +manifest = + { backgroundColor = Just Color.white + , categories = [ Pages.Manifest.Category.education ] + , displayMode = Manifest.Standalone + , orientation = Manifest.Portrait + , description = "elm-pages - A statically typed site generator." + , iarcRatingId = Nothing + , name = "elm-pages docs" + , themeColor = Just Color.white + , startUrl = pages.index + , shortName = Just "elm-pages" + , sourceIcon = images.icon + } +``` + +Lighthouse will also ding you [if you don't have the appropriately sized icons and favicon images](https://developers.google.com/web/tools/lighthouse/audits/manifest-contains-192px-icon). `elm-pages` guarantees that you will follow these best practices (and gives you the confidence that you haven't made any mistakes). It will automatically generate the recommended set of icons and favicons for you, based on a single source image. And, of course, you get a compile-time guarantee that you are using an image that exists! For example, here's what happens if we try to access an image as `logo` when the actual file is called `icon`. + +```haskell +sourceIcon = images.logo +``` + +We then get this elm compiler error: +![Missing image compiler error](/images/compiler-error.png) + +## `elm-pages` is just Elm! + +`elm-pages` hydrates into a full-fledged Elm app (the pre-rendered pages are just for faster loads and better SEO). So you can do whatever you need to using Elm and the typed data that `elm-pages` provides you with. In a future post, I'll explain some of the ways that `elm-pages` leverages the Elm type system for a better developer experience. There's a lot to explore here, this really just scratches the surface! + +## SEO + +One of the main motivations for building `elm-pages` was to make SEO easier and less error-prone. Have you ever seen a link shared on Twitter or elsewhere online that just renders like a plain link? No image, no title, no description. As a user, I'm a little afraid to click those links because I don't have any clues about where it will take me. As a user posting those links, it's very anticlimactic to share the blog post that I lovingly wrote only to see a boring link there in my tweet sharing it with the world. + +I'll also be digging into the topic of SEO in a future post, showing how `elm-pages` makes SEO dead simple. For now, you can take a look at [the built-in `elm-pages` SEO module](https://package.elm-lang.org/packages/dillonkearns/elm-pages/latest/Head-Seo) or take a look at [how this site uses the SEO module](https://github.com/dillonkearns/elm-pages/blob/8448bb60b680fb171319988fb716cb21e0345826/examples/docs/src/Main.elm#L294-L400). + +## Next steps + +There are so many possibilities when you pair Elm with static content! I'm excited to explore this area further with the help of the community. Here are some features that are on my radar. + +- Allow users to pass a set of HTTP requests to fetch during the build step (for making CMS or API data available statically in the build) +- An API to programmatically add pages from metadata (rather than just from files in the `content` folder) +- Allow users to configure the caching strategy for service workers (through pure Elm config of course) +- More SEO features (possibly an API for adding structured data, i.e. JSON-LD, for more interactive and engaging search results) + +And of course, responding to your feedback! Please don't hesitate to share your thoughts, on everything from the documentation to the developer experience. I'd love to hear from you! + +## Getting started with `elm-pages` + +If you'd like to try out `elm-pages` for yourself, or look at some code, the best place to start is the [`elm-pages-starter` repo](https://github.com/dillonkearns/elm-pages-starter). See the site live at [elm-pages-starter.netlify.com](https://elm-pages-starter.netlify.com). Let me know your thoughts on Slack, I'd love to hear from you! Or continue the conversation on Twitter! + + +""" + , staticData = + Dict.fromList + [ ( "{\"method\":\"GET\",\"url\":\"https://api.github.com/repos/dillonkearns/elm-pages\",\"headers\":[],\"body\":{\"type\":\"empty\"}}" + , "{\"stargazers_count\":137}" + ) + ] + + -- Debug.todo "" -- rawContent.staticData + } + in + -- TODO do I need to handle this case? + -- case body of + -- Just presentBody -> + -- Parsed metadata + -- { body = parseContent extension presentBody document + -- , staticData = "" + -- } + -- + -- Nothing -> + --NeedContent extension metadata + thing + ) + |> Tuple.pair path + + Nothing -> + Err ("Could not find extension '" ++ extension ++ "'") + |> Tuple.pair path ) @@ -327,8 +527,8 @@ lazyLoad document url cacheResult = |> Task.map (\downloadedContent -> update cacheResult - (\thing -> - parseContent extension thing document + (\value -> + parseContent extension value document ) url downloadedContent From da4b145d1c5119e4a329118795d9ed50f4d1be07 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Wed, 15 Jan 2020 20:14:37 -0800 Subject: [PATCH 2/8] Add defer to script tags (and preload as well). --- generator/src/develop.js | 5 +++++ package-lock.json | 8 ++++++++ package.json | 1 + 3 files changed, 14 insertions(+) diff --git a/generator/src/develop.js b/generator/src/develop.js index 4382e1d26..3b4b6ecaf 100644 --- a/generator/src/develop.js +++ b/generator/src/develop.js @@ -2,6 +2,7 @@ const webpack = require("webpack"); const middleware = require("webpack-dev-middleware"); const path = require("path"); const HTMLWebpackPlugin = require("html-webpack-plugin"); +const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin'); const CopyPlugin = require("copy-webpack-plugin"); const PrerenderSPAPlugin = require("prerender-spa-plugin"); const merge = require("webpack-merge"); @@ -159,6 +160,10 @@ function webpackOptions( inject: "head", template: path.resolve(__dirname, "template.html") }), + new ScriptExtHtmlWebpackPlugin({ + preload: /\.js$/, + defaultAttribute: 'defer' + }), new FaviconsWebpackPlugin({ logo: path.resolve(process.cwd(), `./${manifestConfig.sourceIcon}`), favicons: { diff --git a/package-lock.json b/package-lock.json index 9fe9efe4b..e19b102c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9220,6 +9220,14 @@ "ajv-keywords": "^3.1.0" } }, + "script-ext-html-webpack-plugin": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/script-ext-html-webpack-plugin/-/script-ext-html-webpack-plugin-2.1.4.tgz", + "integrity": "sha512-7MAv3paAMfh9y2Rg+yQKp9jEGC5cEcmdge4EomRqri10qoczmliYEVPVNz0/5e9QQ202e05qDll9B8zZlY9N1g==", + "requires": { + "debug": "^4.1.1" + } + }, "scss-tokenizer": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", diff --git a/package.json b/package.json index 38b7a9c9f..d51979cad 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "node-sass": "^4.12.0", "prerender-spa-plugin": "^3.4.0", "sass-loader": "^8.0.0", + "script-ext-html-webpack-plugin": "^2.1.4", "style-loader": "^1.0.0", "webpack": "^4.41.5", "webpack-dev-middleware": "^3.7.0", From 829dbc40a9ac08a0fb50d29bfa446deead204c0c Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Thu, 16 Jan 2020 15:59:53 -0800 Subject: [PATCH 3/8] Add a counter that increments with time. --- examples/docs/src/Main.elm | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/examples/docs/src/Main.elm b/examples/docs/src/Main.elm index 6a0b756a4..dba8bd95b 100644 --- a/examples/docs/src/Main.elm +++ b/examples/docs/src/Main.elm @@ -30,6 +30,7 @@ import Pages.Platform exposing (Page) import Pages.StaticHttp as StaticHttp import Palette import Secrets +import Time manifest : Manifest.Config Pages.PathKey @@ -77,16 +78,17 @@ markdownDocument = type alias Model = - {} + { count : Int } init : Maybe (PagePath Pages.PathKey) -> ( Model, Cmd Msg ) init maybePagePath = - ( Model, Cmd.none ) + ( Model 0, Cmd.none ) type Msg = OnPageChange (PagePath Pages.PathKey) + | Tick Time.Posix update : Msg -> Model -> ( Model, Cmd Msg ) @@ -95,10 +97,13 @@ update msg model = OnPageChange page -> ( model, Cmd.none ) + Tick posix -> + ( { model | count = model.count + 1 }, Cmd.none ) + subscriptions : Model -> Sub Msg subscriptions _ = - Sub.none + Time.every 10 Tick view : @@ -176,7 +181,7 @@ pageView stars model siteMetadata page viewForPage = Metadata.Page metadata -> { title = metadata.title , body = - [ header stars page.path + [ header model.count stars page.path , Element.column [ Element.padding 50 , Element.spacing 60 @@ -193,7 +198,7 @@ pageView stars model siteMetadata page viewForPage = { title = metadata.title , body = Element.column [ Element.width Element.fill ] - [ header stars page.path + [ header model.count stars page.path , Element.column [ Element.padding 30 , Element.spacing 40 @@ -224,7 +229,7 @@ pageView stars model siteMetadata page viewForPage = Metadata.Doc metadata -> { title = metadata.title , body = - [ header stars page.path + [ header model.count stars page.path , Element.row [] [ DocSidebar.view page.path siteMetadata |> Element.el [ Element.width (Element.fillPortion 2), Element.alignTop, Element.height Element.fill ] @@ -254,7 +259,7 @@ pageView stars model siteMetadata page viewForPage = Element.column [ Element.width Element.fill ] - [ header stars page.path + [ header model.count stars page.path , Element.column [ Element.padding 30 , Element.spacing 20 @@ -273,7 +278,7 @@ pageView stars model siteMetadata page viewForPage = { title = "elm-pages blog" , body = Element.column [ Element.width Element.fill ] - [ header stars page.path + [ header model.count stars page.path , Element.column [ Element.padding 20, Element.centerX ] [ Index.view siteMetadata ] ] } @@ -300,8 +305,8 @@ articleImageView articleImage = } -header : Int -> PagePath Pages.PathKey -> Element msg -header stars currentPath = +header : Int -> Int -> PagePath Pages.PathKey -> Element msg +header count stars currentPath = Element.column [ Element.width Element.fill ] [ Element.el [ Element.height (Element.px 4) @@ -336,7 +341,8 @@ header stars currentPath = ] } , Element.row [ Element.spacing 15 ] - [ elmDocsLink + [ Element.text <| "Count: " ++ String.fromInt count + , elmDocsLink , githubRepoLink stars --, highlightableLink currentPath pages.docs.directory "Docs" From 34432e518d7f3e9aaa91a44a1154cad853ef6643 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Fri, 17 Jan 2020 17:40:11 -0800 Subject: [PATCH 4/8] Include phase in platform model to do no-op user update function for pre-render phase. --- index.js | 3 ++- src/Pages/Internal/Platform.elm | 34 ++++++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 6a9d6f795..d39ea9cd4 100644 --- a/index.js +++ b/index.js @@ -9,7 +9,8 @@ module.exports = function pagesInit( document.addEventListener("DOMContentLoaded", function() { let app = mainElmModule.init({ flags: { - secrets: null + secrets: null, + isPrerendering: navigator.userAgent.indexOf("Headless") >= 0 } }); diff --git a/src/Pages/Internal/Platform.elm b/src/Pages/Internal/Platform.elm index 5876f213c..575c60552 100644 --- a/src/Pages/Internal/Platform.elm +++ b/src/Pages/Internal/Platform.elm @@ -254,6 +254,17 @@ init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel fla case contentCache of Ok okCache -> let + phase = + case Decode.decodeValue (Decode.field "isPrerendering" Decode.bool) flags of + Ok True -> + Prerender + + Ok False -> + Client + + Err _ -> + Client + ( userModel, userCmd ) = initUserModel maybePagePath @@ -284,6 +295,7 @@ init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel fla , url = url , userModel = userModel , contentCache = contentCache + , phase = phase } , cmd ) @@ -297,6 +309,7 @@ init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel fla , url = url , userModel = userModel , contentCache = contentCache + , phase = Client } , Cmd.batch [ userCmd |> Cmd.map UserMsg @@ -333,9 +346,15 @@ type alias ModelDetails userModel metadata view = , url : Url.Url , contentCache : ContentCache metadata view , userModel : userModel + , phase : Phase } +type Phase + = Prerender + | Client + + update : String -> @@ -524,7 +543,20 @@ application config = \msg outerModel -> case outerModel of Model model -> - update config.canonicalSiteUrl config.view config.pathKey config.onPageChange config.toJsPort config.document config.update msg model + let + userUpdate = + case model.phase of + Prerender -> + noOpUpdate + + Client -> + config.update + + noOpUpdate = + \userMsg userModel -> + ( userModel, Cmd.none ) + in + update config.canonicalSiteUrl config.view config.pathKey config.onPageChange config.toJsPort config.document userUpdate msg model |> Tuple.mapFirst Model |> Tuple.mapSecond (Cmd.map AppMsg) From b5e21de998743ad8e420954bb5ee9dfd4d812a85 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Sun, 19 Jan 2020 13:56:43 -0800 Subject: [PATCH 5/8] Replace hardcoded content.json response in ContentCache with data from flags. --- index.js | 20 ++- src/Pages/ContentCache.elm | 197 ++-------------------------- src/Pages/Internal/Platform.elm | 19 ++- src/Pages/Internal/Platform/Cli.elm | 2 +- 4 files changed, 52 insertions(+), 186 deletions(-) diff --git a/index.js b/index.js index d39ea9cd4..6022a8f4a 100644 --- a/index.js +++ b/index.js @@ -7,10 +7,14 @@ module.exports = function pagesInit( let prefetchedPages = [window.location.pathname]; document.addEventListener("DOMContentLoaded", function() { + + + httpGet(`${window.location.pathname}content.json`, function (/** @type JSON */ contentJson) { let app = mainElmModule.init({ flags: { secrets: null, - isPrerendering: navigator.userAgent.indexOf("Headless") >= 0 + isPrerendering: navigator.userAgent.indexOf("Headless") >= 0, + contentJson } }); @@ -34,6 +38,9 @@ module.exports = function pagesInit( document.dispatchEvent(new Event("prerender-trigger")); }); + + }) + }); function setupLinkPrefetching() { @@ -131,3 +138,14 @@ module.exports = function pagesInit( document.getElementsByTagName("head")[0].appendChild(meta); } }; + +function httpGet(/** @type string */ theUrl, /** @type Function */ callback) +{ + var xmlHttp = new XMLHttpRequest(); + xmlHttp.onreadystatechange = function() { + if (xmlHttp.readyState == 4 && xmlHttp.status == 200) + callback(JSON.parse(xmlHttp.responseText)); + } + xmlHttp.open("GET", theUrl, true); // true for asynchronous + xmlHttp.send(null); +} diff --git a/src/Pages/ContentCache.elm b/src/Pages/ContentCache.elm index 6dcc11268..792824857 100644 --- a/src/Pages/ContentCache.elm +++ b/src/Pages/ContentCache.elm @@ -111,9 +111,10 @@ pagesWithErrors cache = init : Document metadata view -> Content + -> Maybe (ContentJson String) -> ContentCache metadata view -init document content = - parseMetadata document content +init document content maybeInitialPageContent = + parseMetadata maybeInitialPageContent document content |> List.map (\tuple -> Tuple.mapSecond @@ -174,10 +175,11 @@ createBuildError path decodeError = parseMetadata : - Document metadata view + Maybe (ContentJson String) + -> Document metadata view -> List ( List String, { extension : String, frontMatter : String, body : Maybe String } ) -> List ( List String, Result String (Entry metadata view) ) -parseMetadata document content = +parseMetadata maybeInitialPageContent document content = content |> List.map (\( path, { frontMatter, extension, body } ) -> @@ -195,187 +197,16 @@ parseMetadata document content = renderer = \value -> parseContent extension value document - - thing = + in + case maybeInitialPageContent of + Just initialPageContent -> Parsed metadata - { body = renderer """ - -After a round of closed beta testing (thank you to [Brian](https://twitter.com/brianhicks) and the [`elm-conf 2019`](https://2019.elm-conf.com/) organizing team!), I'm excited to share a new static site generator for Elm! - -[Matthew Griffith](https://twitter.com/mech_elephant) and I have had a lot of design discussions and sending code snippets back-and-forth to get to the current design. A big thank you to Matthew for the great discussions and, as always, his ability to look at the bigger picture and question basic assumptions to come up with awesome innovations! - -## What is `elm-pages` exactly? - -Well, this site you're looking at _right now_ is built with `elm-pages`! For example, the raw content for this post is from [`content/blog/introducing-elm-pages.md`](https://github.com/dillonkearns/elm-pages/blob/master/examples/docs/content/blog/introducing-elm-pages.md). - -`elm-pages` takes your static content and turns it into a modern, performant, single-page app. You can do anything you can with a regular Elm site, and yet the framework does a lot for you to optimize site performance and minimize tedious work. - -I see a lot of "roll your own" Elm static sites out there these days. When you roll your own Elm static site, you often: - -- Manage Strings for each page's content (rather than just having a file for each page) -- Wire up the routing for each page manually (or with a hand-made script) -- Add `` tags for SEO and to make Twitter/Slack/etc. link shares display the right image and title (or just skip it because it's a pain) - -I hope that `elm-pages` will make people's lives easier (and their load times faster). But `elm-pages` is for more than just building your blog or portfolio site. There's a movement now called JAMstack (JavaScript, APIs, and Markup) that is solving a broader set of problems with static sites. JAMstack apps do this by pulling in data from external sources, and using modern frontend frameworks to render the content (which then rehydrate into interactive apps). The goal is to move as much work as possible away from the user's browser and into a build step before pushing static files to your CDN host (but without sacrificing functionality). More and more sites are seeing that optimizing performance improves conversion rates and user engagement, and it can also make apps simpler to maintain. - -This is just the first release of `elm-pages`, but I've built a prototype for pulling in external data and am refining the design in preparation for the next release. Once that ships, the use cases `elm-pages` can handle will expand to things like ecommerce sites, job boards, and sites with content written by non-technical content editors. You can find a very informative FAQ and resources page about these ideas at [jamstack.org](https://jamstack.org/) (plus a more in-depth definition of the term JAMstack). - -## Comparing `elm-pages` and `elmstatic` - -`elm-pages` and [`elmstatic`](https://korban.net/elm/elmstatic/) have a lot of differences. At the core, they have two different goals. `elmstatic` generates HTML for you that doesn't include an Elm runtime. It uses Elm as a templating engine to do page layouts. It also makes some assumptions about the structure of your page content, separating `posts` and `pages` and automatically generating post indexes based on the top-level directories within the `posts` folder. It's heavily inspired by traditional static site generators like Jekyll. - -`elm-pages` hydrates into a single-page app that includes a full Elm runtime, meaning that you can have whatever client-side interactivity you want. It supports similar use cases to static site generators like [Gatsby](http://gatsbyjs.org). `elm-pages` makes a lot of optimizations by splitting and lazy-loading pages, optimizing image assets, and using service workers for repeat visits. It pre-renders HTML for fast first renders, but because it ships with JavaScript code it is also able to do some performance optimizations to make page changes faster (and without page flashes). So keep in mind that shipping without JavaScript doesn't necessarily mean your site performance suffers! You may have good reasons to want a static site with no JavaScript, but open up a Lighthouse audit and try it out for yourself rather than speculating about performance! - -Either framework might be the right fit depending on your goals. I hope this helps illuminate the differences! - -## How does `elm-pages` work? - -The flow is something like this: - -- Put your static content in your `content` folder (it could be Markdown, `elm-markup`, or something else entirely) -- Register Elm functions that define what to do with the [frontmatter](https://jekyllrb.com/docs/front-matter/) (that YAML data at the top of your markup files) and the body of each type of file you want to handle -- Define your app's configuration in pure Elm (just like a regular Elm `Browser.application` but with a few extra functions for SEO and site configuration) -- Run `elm-pages build` and ship your static files (JS, HTML, etc.) to Netlify, Github Pages, or your CDN of choice! - -The result is a blazing fast static site that is optimized both for the first load experience, and also uses some caching strategies to improve site performance for repeat visitors. You can look in your dev tools or run a Lighthouse audit on this page to see some of the performance optimizations `elm-pages` does for you! - -The way you set up an `elm-pages` app will look familiar if you have some experience with wiring up standard Elm boilerplate: - -```elm -main : Pages.Platform.Program Model Msg Metadata (List (Element Msg)) -main = - Pages.application - { init = init - , view = view - , update = update - , subscriptions = subscriptions - , documents = [ markdownHandler ] - , head = head - , manifest = manifest - , canonicalSiteUrl = "https://elm-pages.com" - } -``` - -You can take a look at [the `Main.elm` file for this site](https://github.com/dillonkearns/elm-pages/blob/master/examples/docs/src/Main.elm#L52) to get a better sense of the bigger picture. I'll do a more in-depth explanation of this setup in a future post. The short version is that - -`init`, `update`, and `subscriptions` are as you would expect (but maybe a bit simpler since `elm-pages` manages things like the URL for you). - -`documents` are where you define how to handle the frontmatter and body of the files in your `content` folder. And the `view` function gives you the result from your frontmatter and body, as well as your `Model`. - -`head` is just a function that passes you the metadata for a given page and lets you define tags to put in the `` (mostly for SEO). - -`manifest` lets you configure some settings that allow your app to be installed for offline use. - -And the end result is that `elm-pages` gets everything it needs about your site in order to optimize it and turn it into a modern, performant site that will get a great Lighthouse audit score! The goal is to make following best practices for a modern, performant static site one of the following: - -- Built-in -- Enforced by the Elm compiler -- Or at the very least the path of least resistence - -## What makes Elm awesome for building static sites - -JAMstack frameworks, like [Gatsby](http://gatsbyjs.org), can make powerful optimizations because they are dealing with strong constraints (specifically, content that is known at build time). Elm is the perfect tool for the JAMstack because it can leverage those constraints and turn them into compiler guarantees. Not only can we do more with static guarantees using Elm, but we can get additional guarantees using Elm's type-system and managed side-effects. It's a virtuous cycle that enables a lot of innovation. - -## Why use `elm-pages`? - -Let's take a look at a few of the features that make `elm-pages` worthwhile for the users (both the end users, and the team using it to build their site). - -### Performance - -- Pre-rendered pages for blazing fast first renders and improved SEO -- Your content is loaded as a single-page app behind the scenes, giving you smooth page changes -- Split individual page content and lazy load each page -- Prefetch page content on link hover so page changes are almost instant -- Image assets are optimized -- App skeleton is cached with a service worker (with zero configuration) so it's available offline - -One of the early beta sites that used `elm-pages` instantly shaved off over a megabyte for the images on a single page! Optimizations like that need to be built-in and automatic otherwise some things inevitably slip through the cracks. - -### Type-safety and simplicity - -- The type system guarantees that you use valid images and routes in the right places -- You can even set up a validation to give build errors if there are any broken links or images in your markdown -- You can set up validations to define your own custom rules for your domain! (Maximum title length, tag name from a set to avoid multiple tags with different wording, etc.) - -## Progressive Web Apps - -[Lighthouse recommends having a Web Manifest file](https://developers.google.com/web/tools/lighthouse/audits/manifest-exists) for your app to allow users to install the app to your home screen and have an appropriate icon, app name, etc. -Elm pages gives you a type-safe way to define a web manifest for your app: - -```elm -manifest : Manifest.Config PagesNew.PathKey -manifest = - { backgroundColor = Just Color.white - , categories = [ Pages.Manifest.Category.education ] - , displayMode = Manifest.Standalone - , orientation = Manifest.Portrait - , description = "elm-pages - A statically typed site generator." - , iarcRatingId = Nothing - , name = "elm-pages docs" - , themeColor = Just Color.white - , startUrl = pages.index - , shortName = Just "elm-pages" - , sourceIcon = images.icon - } -``` - -Lighthouse will also ding you [if you don't have the appropriately sized icons and favicon images](https://developers.google.com/web/tools/lighthouse/audits/manifest-contains-192px-icon). `elm-pages` guarantees that you will follow these best practices (and gives you the confidence that you haven't made any mistakes). It will automatically generate the recommended set of icons and favicons for you, based on a single source image. And, of course, you get a compile-time guarantee that you are using an image that exists! For example, here's what happens if we try to access an image as `logo` when the actual file is called `icon`. - -```haskell -sourceIcon = images.logo -``` - -We then get this elm compiler error: -![Missing image compiler error](/images/compiler-error.png) - -## `elm-pages` is just Elm! - -`elm-pages` hydrates into a full-fledged Elm app (the pre-rendered pages are just for faster loads and better SEO). So you can do whatever you need to using Elm and the typed data that `elm-pages` provides you with. In a future post, I'll explain some of the ways that `elm-pages` leverages the Elm type system for a better developer experience. There's a lot to explore here, this really just scratches the surface! - -## SEO - -One of the main motivations for building `elm-pages` was to make SEO easier and less error-prone. Have you ever seen a link shared on Twitter or elsewhere online that just renders like a plain link? No image, no title, no description. As a user, I'm a little afraid to click those links because I don't have any clues about where it will take me. As a user posting those links, it's very anticlimactic to share the blog post that I lovingly wrote only to see a boring link there in my tweet sharing it with the world. - -I'll also be digging into the topic of SEO in a future post, showing how `elm-pages` makes SEO dead simple. For now, you can take a look at [the built-in `elm-pages` SEO module](https://package.elm-lang.org/packages/dillonkearns/elm-pages/latest/Head-Seo) or take a look at [how this site uses the SEO module](https://github.com/dillonkearns/elm-pages/blob/8448bb60b680fb171319988fb716cb21e0345826/examples/docs/src/Main.elm#L294-L400). - -## Next steps - -There are so many possibilities when you pair Elm with static content! I'm excited to explore this area further with the help of the community. Here are some features that are on my radar. - -- Allow users to pass a set of HTTP requests to fetch during the build step (for making CMS or API data available statically in the build) -- An API to programmatically add pages from metadata (rather than just from files in the `content` folder) -- Allow users to configure the caching strategy for service workers (through pure Elm config of course) -- More SEO features (possibly an API for adding structured data, i.e. JSON-LD, for more interactive and engaging search results) - -And of course, responding to your feedback! Please don't hesitate to share your thoughts, on everything from the documentation to the developer experience. I'd love to hear from you! - -## Getting started with `elm-pages` - -If you'd like to try out `elm-pages` for yourself, or look at some code, the best place to start is the [`elm-pages-starter` repo](https://github.com/dillonkearns/elm-pages-starter). See the site live at [elm-pages-starter.netlify.com](https://elm-pages-starter.netlify.com). Let me know your thoughts on Slack, I'd love to hear from you! Or continue the conversation on Twitter! - - -""" - , staticData = - Dict.fromList - [ ( "{\"method\":\"GET\",\"url\":\"https://api.github.com/repos/dillonkearns/elm-pages\",\"headers\":[],\"body\":{\"type\":\"empty\"}}" - , "{\"stargazers_count\":137}" - ) - ] - - -- Debug.todo "" -- rawContent.staticData + { body = renderer initialPageContent.body + , staticData = initialPageContent.staticData } - in - -- TODO do I need to handle this case? - -- case body of - -- Just presentBody -> - -- Parsed metadata - -- { body = parseContent extension presentBody document - -- , staticData = "" - -- } - -- - -- Nothing -> - --NeedContent extension metadata - thing + + Nothing -> + NeedContent extension metadata ) |> Tuple.pair path diff --git a/src/Pages/Internal/Platform.elm b/src/Pages/Internal/Platform.elm index 575c60552..84672ec7c 100644 --- a/src/Pages/Internal/Platform.elm +++ b/src/Pages/Internal/Platform.elm @@ -217,6 +217,12 @@ type alias Flags = Decode.Value +type alias ContentJson = + { body : String + , staticData : Dict String String + } + + init : pathKey -> String @@ -249,7 +255,18 @@ init : init pathKey canonicalSiteUrl document toJsPort viewFn content initUserModel flags url key = let contentCache = - ContentCache.init document content + ContentCache.init document content contentJson + + contentJson = + flags + |> Decode.decodeValue (Decode.field "contentJson" contentJsonDecoder) + |> Result.toMaybe + + contentJsonDecoder : Decode.Decoder ContentJson + contentJsonDecoder = + Decode.map2 ContentJson + (Decode.field "body" Decode.string) + (Decode.field "staticData" (Decode.dict Decode.string)) in case contentCache of Ok okCache -> diff --git a/src/Pages/Internal/Platform/Cli.elm b/src/Pages/Internal/Platform/Cli.elm index ba87f3fb3..a0e745444 100644 --- a/src/Pages/Internal/Platform/Cli.elm +++ b/src/Pages/Internal/Platform/Cli.elm @@ -160,7 +160,7 @@ cliApplication : cliApplication cliMsgConstructor narrowMsg toModel fromModel config = let contentCache = - ContentCache.init config.document config.content + ContentCache.init config.document config.content Nothing siteMetadata = contentCache From db2c480128589a5234f2c36266423aab408d18d8 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Sun, 19 Jan 2020 14:04:30 -0800 Subject: [PATCH 6/8] Temporarily remove millisecond counter to compare performance with master. --- examples/docs/src/Main.elm | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/docs/src/Main.elm b/examples/docs/src/Main.elm index dba8bd95b..2663d417c 100644 --- a/examples/docs/src/Main.elm +++ b/examples/docs/src/Main.elm @@ -103,7 +103,8 @@ update msg model = subscriptions : Model -> Sub Msg subscriptions _ = - Time.every 10 Tick + --Time.every 10 Tick + Sub.none view : @@ -341,8 +342,8 @@ header count stars currentPath = ] } , Element.row [ Element.spacing 15 ] - [ Element.text <| "Count: " ++ String.fromInt count - , elmDocsLink + [ --Element.text <| "Count: " ++ String.fromInt count, + elmDocsLink , githubRepoLink stars --, highlightableLink currentPath pages.docs.directory "Docs" From de6594e66c1e4baa8135f3c96e0701d59817ed68 Mon Sep 17 00:00:00 2001 From: Dillon Kearns Date: Mon, 20 Jan 2020 07:30:05 -0800 Subject: [PATCH 7/8] Make content.json relative paths more reliable. --- generator/src/template.html | 2 +- index.js | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/generator/src/template.html b/generator/src/template.html index 5859a4725..085202258 100644 --- a/generator/src/template.html +++ b/generator/src/template.html @@ -1,7 +1,7 @@ - +