Browsing Tag: Generator

    web design

    Context And Variables In The Hugo Static Site Generator — Smashing Magazine

    02/18/2021

    About The Author

    Kristian does web development and writing as part of the team behind the Tower Git client. Based on an island in Southwest Finland, he enjoys running, reading …
    More about
    Kristian

    In this article, we take a look at the topic of context and variables in Hugo, a popular static site generator. You’ll understand concepts such as the global context, flow control, and variables in Hugo templates, as well as data flow from content files through templates to partials and base templates.

    In this article, we’ll take a close look at how context works in the Hugo static site generator. We’ll examine how data flows from content to templates, how certain constructs change what data is available, and how we can pass on this data to partials and base templates.

    This article is not an introduction to Hugo. You’ll probably get the most out of it if you have some experience with Hugo, as we won’t go over every concept from scratch, but rather focus on the main topic of context and variables. However, if you refer to the Hugo documentation throughout, you may well be able to follow along even without previous experience!

    We’ll study various concepts by building up an example page. Not every single file required for the example site will be covered in detail, but the complete project is available on GitHub. If you want to understand how the pieces fit together, that’s a good starting point. Please also note that we won’t cover how to set up a Hugo site or run the development server — instructions for running the example are in the repository.

    What Is A Static Site Generator?

    If the concept of static site generators is new to you, here’s a quick introduction! Static site generators are perhaps best described by comparing them to dynamic sites. A dynamic site like a CMS generally assembles a page from scratch for each visit, perhaps fetching data from a database and combining various templates to do so. In practice, the use of caching means the page is not regenerated quite so often, but for the purpose of this comparison, we can think of it that way. A dynamic site is well suited to dynamic content: content that changes often, content that’s presented in a lot of different configurations depending on input, and content that can be manipulated by the site visitor.

    In contrast, many sites rarely change and accept little input from visitors. A “help” section for an application, a list of articles or an eBook could be examples of such sites. In this case, it makes more sense to assemble the final pages once when the content changes, thereafter serving the same pages to every visitor until the content changes again.

    Dynamic sites have more flexibility, but place more demand on the server they’re running on. They can also be difficult to distribute geographically, especially if databases are involved. Static site generators can be hosted on any server capable of delivering static files, and are easy to distribute.

    A common solution today, which mixes these approaches, is the JAMstack. “JAM” stands for JavaScript, APIs and markup and describes the building blocks of a JAMstack application: a static site generator generates static files for delivery to the client, but the stack has a dynamic component in the form of JavaScript running on the client — this client component can then use APIs to provide dynamic functionality to the user.

    Hugo

    Hugo is a popular static site generator. It’s written in Go, and the fact that Go is a compiled programming language hints at some of Hugos benefits and drawbacks. For one, Hugo is very fast, meaning that it generates static sites very quickly. Of course, this has no bearing on how fast or slow the sites created using Hugo are for the end user, but for the developer, the fact that Hugo compiles even large sites in the blink of an eye is quite valuable.

    However, as Hugo is written in a compiled language, extending it is difficult. Some other site generators allow you to insert your own code — in languages like Ruby, Python or JavaScript — into the compilation process. To extend Hugo, you would need to add your code to Hugo itself and recompile it — otherwise, you’re stuck with the template functions Hugo comes with out-of-the-box.

    While it does provide a rich variety of functions, this fact can become limiting if the generation of your pages involves some complicated logic. As we found, having a site originally developed running on a dynamic platform, the cases where you’ve taken the ability to drop in your custom code for granted do tend to pile up.

    Our team maintains a variety of web sites relating to our main product, the Tower Git client, and we’ve recently looked at moving some of these over to a static site generator. One of our sites, the “Learn” site, looked like a particularly nice fit for a pilot project. This site contains a variety of free learning material including videos, eBooks and FAQs on Git, but also other tech topics.

    Its content is largely of a static nature, and whatever interactive features there are (like newsletter sign-ups) were already powered by JavaScript. At the end of 2020, we converted this site from our previous CMS to Hugo, and today it runs as a static site. Naturally, we learned a lot about Hugo during this process. This article is a way of sharing some of the things we learned.

    Our Example

    As this article grew out of our work on converting our pages to Hugo, it seems natural to put together a (very!) simplified hypothetical landing page as an example. Our main focus will be a reusable so-called “list” template.

    In short, Hugo will use a list template for any page that contains subpages. There’s more to Hugos template hierarchy than that, but you don’t have to implement every possible template. A single list template goes a long way. It will be used in any situation calling for a list template where no more specialized template is available.

    Potential use cases include a home page, a blog index or a list of FAQs. Our reusable list template will reside in layouts/_default/list.html in our project. Again, the rest of the files needed to compile our example are available on GitHub, where you can also get a better look at how the pieces fit together. The GitHub repository also comes with a single.html template — as the name suggests, this template is used for pages that do not have subpages, but act as single pieces of content in their own right.

    Now that we’ve set the stage and explained what it is we’ll be doing, let’s get started!

    The Context Or “The Dot”

    It all starts with the dot. In a Hugo template, the object . — “the dot” — refers to the current context. What does this mean? Every template rendered in Hugo has access to a set of data — its context. This is initially set to an object representing the page currently being rendered, including its content and some metadata. The context also includes site-wide variables like configuration options and information about the current environment. You’d access a field like the title of the current page using .Title and the version of Hugo being used through .Hugo.Version — in other words, you’re accessing fields of the . structure.

    Importantly, this context can change, making a reference like `.Title` above point at something else or even making it invalid. This happens, for example, as you loop over a collection of some kind using range, or as you **split templates into partials and base templates**. We’ll look at this in detail later!

    Hugo uses the Go “templates” package, so when we refer to Hugo templates in this article, we’re really talking about Go templates. Hugo does add a lot template functions not available in standard Go templates.

    In my opinion, the context and the possibility to rebind it is one of Hugos best features. To me, it makes a lot of sense to always have “the dot” represent whatever object is the main focus of my template at a certain point, rebinding it as necessary as I go along. Of course, it’s possible to get yourself into a tangled mess as well, but I’ve been happy with it so far, to the extent that I quickly started missing it in any other static site generator I looked at.

    With this, we’re ready to look out at the humble starting point of our example — the template below, residing in the location layouts/_default/list.html in our project:

    <html>
      <head>
        <title>{{ .Title }} | {{ .Site.Title }}</title>
        <link rel="stylesheet" href="http://www.smashingmagazine.com/css/style.css">
      </head>
      <body>
        <nav>
          <a class="logo" href="{{ "/" | relURL }}">
            <img src="http://www.smashingmagazine.com/img/tower-logo.svg">
            <img src="/img/tower-claim.svg">
          </a>
          <ul>
            <li><a href="/">Home</a></li>
          </ul>
        </nav>
        <section class="content">
          <div class="container">
            <h1>{{ .Title }}</h1>
            {{ .Content }}
          </div>
        </section>
      </body>
    </html>
    

    Most of the template consists of a bare-bones HTML structure, with a stylesheet link, a menu for navigation and some extra elements and classes used for styling. The interesting stuff is between the curly braces, which signal Hugo to step in and do its magic, replacing whatever is between the braces with the result of evaluating some expression and potentially manipulating the context as well.

    As you may be able to guess, {{ .Title }} in the title tag refers to the title of the current page, while {{ .Site.Title }} refers to the title for the whole site, set in the Hugo configuration. A tag like {{ .Title }} simply tells Hugo to replace that tag with the contents of the field Title in the current context.

    So, we’ve accessed some data belonging to the page in a template. Where does this data come from? That’s the topic of the following section.

    Content And Front Matter

    Some of the variables available in the context are automatically provided by Hugo. Others are defined by us, mainly in content files. There are also other sources of data like configuration files, environment variables, data files and even APIs. In this article our focus will be on content files as the source of data.

    In general, a single content file represents a single page. A typical content file includes the main content of that page but also metadata about the page, like its title or the date it was created. Hugo supports several formats both for the main content and the metadata. In this article we’ll go with perhaps the most common combination: the content is provided as Markdown in a file containing the metadata as YAML front matter.

    In practice, that means the content file starts with a section delimited by a line containing three dashes at each end. This section constitutes the front matter, and here metadata is defined using a key: value syntax (As we’ll see soon, YAML supports more elaborate data structures too). The front matter is followed by the actual content, specified using the Markdown markup language.

    Let’s make things more concrete by looking at an example. Here’s a very simple content file with one front matter field and one paragraph of content:

    ---
    title: Home
    ---
    
    Home page of the Tower Git client. Over 100,000 developers and designers use Tower to be more productive!
    

    (This file resides at content/_index.md in our project, with _index.md denoting the content file for a page that has subpages. Again, the GitHub repository makes it clear where which file is supposed to go.)

    Rendered using the template from earlier, along with some styles and peripheral files (all found on GitHub), the result looks like this:

    (Large preview)

    You may wonder whether the field names in the front matter of our content file are predetermined, or whether we can add any field we like. The answer is “both”. There is a list of predefined fields, but we can also add any other field we can come up with. However, these fields are accessed a bit differently in the template. While a predefined field like title is accessed simply as .Title, a custom field like author is accessed using .Params.author.

    (For a quick reference on the predefined fields, along with things like functions, function parameters and page variables, see our own Hugo cheat sheet!)

    The .Content variable, used to access the main content from the content file in your template, is special. Hugo has a “shortcode” feature allowing you to use some extra tags in your Markdown content. You can also define your own. Unfortunately, these shortcodes will only work through the .Content variable — while you can run any other piece of data through a Markdown filter, this will not handle the shortcodes in the content.

    A note here about undefined variables: accessing a predefined field like .Date always works, even though you haven’t set it — an empty value will be returned in this case. Accessing an undefined custom field, like .Params.thisHasNotBeenSet, also works, returning an empty value. However, accessing a non-predefined top-level field like .thisDoesNotExist will prevent the site from compiling.

    As indicated by .Params.author as well as .Hugo.version and .Site.title earlier, chained invocations can be used to access a field nested in some other data structure. We can define such structures in our front matter. Let’s look at an example, where we define a map, or dictionary, specifying some properties for a banner on the page in our content file. Here is the updated content/_index.md:

    ---
    title: Home
    banner:
      headline: Try Tower For Free!
      subline: Download our trial to try Tower for 30 days
    ---
    
    Home page of the Tower Git client. Over 100,000 developers and designers use Tower to be more productive!
    

    Now, let’s add a banner to our template, referring to the banner data using .Params the way described above:

    <html>
      ...
      <body>
        ...
        <aside>
          <h2>{{ .Params.banner.headline }}</h2>
          <p>{{ .Params.banner.subline}}</p>
        </aside>
      </body>
    </html>
    

    Here’s what our site looks like now:

    (Large preview)

    All right! At the moment, we’re accessing fields of the default context without any issues. However, as mentioned earlier, this context is not fixed, but can change. Let’s look at how that might happen.

    Flow Control

    Flow control statements are an important part of a templating language, allowing you do different things depending on the value of variables, loop through data and more. Hugo templates provide the expected set of constructs, including if/else for conditional logic, and range for looping. Here, we will not cover flow control in Hugo in general (for more on this, see the documentation), but focus on how these statements affect the context. In this case, the most interesting statements are with and range.

    Let’s start with with. This statement checks if some expression has a “non-empty” value, and, if it has, rebinds the context to refer to the value of that expression. An end tag indicates the point where the influence of the with statement stops, and the context is rebound to whatever it was before. The Hugo documentation defines a non-empty value as false, 0, and any zero-length array, slice, map or string.

    Currently, our list template is not doing much listing at all. It might make sense for a list template to actually feature some of its subpages in some way. This gives us a perfect opportunity for examples of our flow control statements.

    Perhaps we want to display some featured content at the top of our page. This could be any piece of content — a blog post, a help article or a recipe, for example. Right now, let’s say our Tower example site has some pages highlighting its features, use-cases, a help page, a blog page, and a “learning platform” page. These are all located in the content/ directory. We configure which piece of content to feature by adding a field in the content file for our home page, content/_index.md. The page is referred to by its path, assuming the content directory as root, like so:

    ---
    title: Home
    banner:
      headline: Try Tower For Free!
      subline: Download our trial to try Tower for 30 days without limitations
    featured: /features.md
    ...
    ---
    ...
    

    Next, our list template has to be modified to display this piece of content. Hugo has a template function, .GetPage, which will allow us to refer to page objects other than the one we’re currently rendering. Recall how the context, ., was initially bound to an object representing the page being rendered? Using .GetPage and with, we can temporarily rebind the context to another page, referring to the fields of that page when displaying our featured content:

    <nav>
      ...
    </nav>
    <section class="featured">
      <div class="container">
        {{ with .GetPage .Params.featured }}
          <article>
            <h2>{{ .Title }}</h2>
            {{ .Summary }}
            <p><a href="{{ .Permalink }}">Read more →</a></p>
          </article>
        {{ end }}
      </div>
    </section>
    

    Here, {{ .Title }}, {{ .Summary }} and {{ .Permalink }} between the with and the end tags refer to those fields in the featured page, and not the main one being rendered.

    In addition to having a featured piece of content, let’s list a few more pieces of content further down. Just like the featured content, the listed pieces of content will be defined in content/_index.md, the content file for our home page. We’ll add a list of content paths to our front matter like this (in this case also specifying the section headline):

    ---
    ...
    listing_headline: Featured Pages
    listing:
      - /help.md
      - /use-cases.md
      - /blog/_index.md
      - /learn.md
    ---
    

    The reason that the blog page has its own directory and an _index.md file is that the blog will have subpages of its own — blog posts.

    To display this list in our template, we’ll use range. Unsurprisingly, this statement will loop over a list, but it will also rebind the context to each element of the list in turn. This is very convenient for our content list.

    Note that, from the perspective of Hugo, “listing” only contains some strings. For each iteration of the “range” loop, the context will be bound to one of those strings. To get access to the actual page object, we supply its path string (now the value of .) as an argument to .GetPage. Then, we’ll use the with statement again to rebind the context to the listed page object rather than its path string. Now, it’s easy to display the content of each listed page in turn:

    <aside>
      ...
    </aside>
    <section class="listing">
      <div class="container">
        <h1>{{ .Params.listing_headline }}</h1>
        <div>
          {{ range .Params.listing }}
            {{ with $.GetPage . }}
              <article>
                <h2>{{ .Title }}</h2>
                {{ .Summary }}
                <p><a href="{{ .Permalink }}">Read more →</a></p>
              </article>
            {{ end }}
          {{ end }}
        </div>
      </div>
    </section>
    

    Here’s what the site looks like at this point:

    (Large preview)

    But hold on, there’s something weird in the template above — rather than calling .GetPage, we’re calling $.GetPage. Can you guess why .GetPage wouldn’t work?

    The notation .GetPage indicates that the GetPage function is a method of the current context. Indeed, in the default context, there is such a method, but we’ve just gone ahead and changed the context! When we call .GetPage, the context is bound to a string, which does not have that method. The way we work around this is the topic of the next section.

    The Global Context

    As seen above, there are situations where the context has been changed, but we’d still like to access the original context. Here, it’s because we want to call a method existing in the original context — another common situation is when we want to access some property of the main page being rendered. No problem, there’s an easy way to do this.

    In a Hugo template, $, known as the global context, refers to the original value of the context — the context as it was when template processing started. In the previous section, it was used to call the .GetPage method even though we had rebound the context to a string. Now, we’ll also use it to access a field of the page being rendered.

    At the beginning of this article, I mentioned that our list template is reusable. So far, we’ve only used it for the home page, rendering a content file located at content/_index.md. In the example repository, there is another content file which will be rendered using this template: content/blog/_index.md. This is an index page for the blog, and just like the home page it shows a featured piece of content and lists a few more — blog posts, in this case.

    Now, let’s say we want to show listed content slightly differently on the home page — not enough to warrant a separate template, but something we can do with a conditional statement in the template itself. As an example, we’ll display the listed content in a two-column grid, as opposed to a single-column list, if we detect that we’re rendering the home page.

    Hugo comes with a page method, .IsHome, which provides exactly the functionality we need. We’ll handle the actual change in presentation by adding a class to the individual pieces of content when we find we’re on the home page, allowing our CSS file do the rest.

    We could, of course, add the class to the body element or some containing element instead, but that wouldn’t enable as good a demonstration of the global context. By the time we write the HTML for the listed piece of content, . refers to the listed page, but IsHome needs to be called on the main page being rendered. The global context comes to our rescue:

    <section class="listing">
      <div class="container">
        <h1>{{ .Params.listing_headline }}</h1>
        <div>
          {{ range .Params.listing }}
            {{ with $.GetPage . }}
              <article{{ if $.IsHome }} class="home"{{ end }}>
                <h2>{{ .Title }}</h2>
                {{ .Summary }}
                <p><a href="{{ .Permalink }}">Read more →</a></p>
              </article>
            {{ end }}
          {{ end }}
        </div>
      </div>
    </section>
    

    The blog index looks just like our home page did, albeit with different content:

    (Large preview)

    …but our home page now displays its featured content in a grid:

    (Large preview)

    Partial Templates

    When building up a real website, it quickly becomes useful to split your templates into parts. Perhaps you want to reuse some particular part of a template, or perhaps you just want to split a huge, unwieldy template into coherent pieces. For this purpose, Hugo’s partial templates are the way to go.

    From a context perspective, the important thing here is that when we include a partial template, we explicitly pass it the context we want to make available to it. A common practice is to pass in the context as it is when the partial is included, like this: {{ partial "my/partial.html" . }}. If the dot here refers to the page being rendered, that’s what will be passed to the partial; if the context has been rebound to something else, that’s what’s passed down.

    You can, of course, rebind the context in partial templates just like in normal ones. In this case, the global context, $, refers to the original context passed to the partial, not the main page being rendered (unless that’s what was passed in).

    If we want a partial template to have access to some particular piece of data, we might run into problems if we pass only this to the partial. Recall our problem earlier with accessing page methods after rebinding the context? The same goes for partials, but in this case the global context can’t help us — if we’ve passed in, say, a string to a partial template, the global context in the partial will refer to that string, and we won’t be able to call methods defined on the page context.

    The solution to this problem lies in passing in more than one piece of data when including the partial. However, we’re only allowed to provide one argument to the partial call. We can, however, make this argument a compund data type, commonly a map (known as a dictionary or a hash in other programming languages).

    In this map, we can, for example, have a Page key set to the current page object, along with other keys for any custom data to pass in. The page object will then be available as .Page in the partial, and the other values of the map are accessed similarly. A map is created using the dict template function, which takes an even number of arguments, interpreted alternately as a key, its value, a key, its value and so on.

    In our example template, let’s move the code for our featured and listed content into partials. For the featured content, it’s enough to pass in the featured page object. The listed content, however, needs access to the .IsHome method in addition to the particular listed content being rendered. As mentioned earlier, while .IsHome is available on the page object for the listed page as well, that won’t give us the correct answer — we want to know if the main page being rendered is the home page.

    We could instead pass in a boolean set to the result of calling .IsHome, but perhaps the partial will need access to other page methods in the future — let’s go with passing in the main page object as well as the listed page object. In our example, the main page is found in $ and the listed page in .. So, in the map passed to the listed partial, the key Page gets the value $ while the key “Listed” gets the value .. This is the updated main template:

    <body>
      <nav>
        <a class="logo" href="{{ "/" | relURL }}">
          <img src="http://www.smashingmagazine.com/img/tower-logo.svg">
          <img src="/img/tower-claim.svg">
        </a>
        <ul>
          <li><a href="/">Home</a></li>
          <li><a href="http://www.smashingmagazine.com/blog/">Blog</a></li>
        </ul>
      </nav>
      <section class="featured">
        <div class="container">
          {{ with .GetPage .Params.featured }}
            {{ partial "partials/featured.html" . }}
          {{ end }}
        </div>
      </section>
      <section class="content">
        <div class="container">
          <h1>{{ .Title }}</h1>
          {{ .Content }}
        </div>
      </section>
      <aside>
        <h2>{{ .Params.banner.headline }}</h2>
        <p>{{ .Params.banner.subline}}</p>
      </aside>
      <section class="listing">
        <div class="container">
          <h1>{{ .Params.listing_headline }}</h1>
          <div>
            {{ range .Params.listing }}
              {{ with $.GetPage . }}
                {{ partial "partials/listed.html" (dict "Page" $ "Listed" .) }}
              {{ end }}
            {{ end }}
          </div>
        </div>
      </section>
    </body>
    

    The content of our “featured” partial does not change compared to when it was part of the list template:

    <article>
      <h2>{{ .Title }}</h2>
      {{ .Summary }}
      <p><a href="{{ .Permalink }}">Read more →</a></p>
    </article>
    

    Our partial for listed content, however, reflects the fact that the original page object is now found in .Page while the listed piece of content is found in .Listed:

    <article{{ if .Page.IsHome }} class="home"{{ end }}>
      <h2>{{ .Listed.Title }}</h2>
      {{ .Listed.Summary }}
      <p><a href="{{ .Listed.Permalink }}">Read more →</a></p>
    </article>
    

    Hugo also provides base template functionality which lets you extend a common base template, as opposed to including subtemplates. In this case, the context works similarly: when extending a base template, you provide the data that will constitute the original context in that template.

    Custom Variables

    It is also possible to assign and reassign your own custom variables in a Hugo template. These will be available in the template where they’re declared, but won’t make their way into any partials or base templates unless we explicitly pass them on. A custom variable declared inside a “block” like the one specified by an if statement will only be available inside that block — if we want to refer to it outside the block, we need to declare it outside the block, then modify it inside the block as required.

    Custom variables have names prefixed by a dollar sign ($). To declare a variable and give it a value at the same time, use the := operator. Subsequent assignments to the variable use the = operator (without colon). A variable can’t be assigned to before being declared, and it can’t be declared without giving it a value.

    One use case for custom variables is simplifying long function calls by assigning some intermediate result to an appropriately named variable. For example, we could assign the featured page object to a variable named $featured and then supply this variable to the with statement. We could also put the data to supply to the “listed” partial in a variable and give that to the partial call.

    Here’s what our template would look like with those changes:

    <section class="featured">
      <div class="container">
        {{ $featured := .GetPage .Params.featured }}
        {{ with $featured }}
          {{ partial "partials/featured.html" . }}
        {{ end }}
      </div>
    </section>
    <section class="content">
      ...
    </section>
    <aside>
      ...
    </aside>
    <section class="listing">
      <div class="container">
        <h1>{{ .Params.listing_headline }}</h1>
        <div>
          {{ range .Params.listing }}
            {{ with $.GetPage . }}
              {{ $context := (dict "Page" $ "Listed" .) }}
              {{ partial "partials/listed.html" $context }}
            {{ end }}
          {{ end }}
        </div>
      </div>
    </section>
    

    Based on my experience with Hugo, I’d recommend using custom variables liberally as soon as you’re trying to implement some more involved logic in a template. While it’s natural to try to keep your code concise, this may easily make things less clear than they could be, confusing you and others.

    Instead, use descriptively named variables for each step and don’t worry about using two lines (or three, or four, etc.) where one would do.

    .Scratch

    Finally, let’s cover the .Scratch mechanism. In earlier versions of Hugo, custom variables could only be assigned to once; it was not possible to redefine a custom variable. Nowadays, custom variables can be redefined, which makes .Scratch less important, though it still has its uses.

    In short, .Scratch is a scratch area allowing you to set and modify your own variables, like custom variables. Unlike custom variables, .Scratch belongs to the page context, so passing that context on to a partial, for example, will bring the scratch variables along with it automatically.

    You can set and retrieve variables on .Scratch by calling its methods Set and Get. There are more methods than these, for example for setting and updating compound data types, but these two ones will suffice for our needs here. Set takes two parameters: the key and the value for the data you want to set. Get only takes one: the key for the data you want to retrieve.

    Earlier, we used dict to create a map data structure to pass multiple pieces of data to a partial. This was done so that the partial for a listed page would have access to both the original page context and the particular listed page object. Using .Scratch is not necessarily a better or worse way to do this — whichever is preferrable may depend on the situation.

    Let’s see what our list template would look like using .Scratch instead of dict to pass data to the partial. We call $.Scratch.Get (again using the global context) to set the scratch variable “listed” to . — in this case, the listed page object. Then we pass in just the page object, $, to the partial. The scratch variables will follow along automatically.

    <section class="listing">
      <div class="container">
        <h1>{{ .Params.listing_headline }}</h1>
        <div>
          {{ range .Params.listing }}
            {{ with $.GetPage . }}
              {{ $.Scratch.Set "listed" . }}
              {{ partial "partials/listed.html" $ }}
            {{ end }}
          {{ end }}
        </div>
      </div>
    </section>
    

    This would require some modification to the listed.html partial as well — the original page context is now available as “the dot” while the listed page is retrieved from the .Scratch object. We’ll use a custom variable to simplify access to the listed page:

    <article{{ if .IsHome }} class="home"{{ end }}>
      {{ $listed := .Scratch.Get "listed" }}
      <h2>{{ $listed.Title }}</h2>
      {{ $listed.Summary }}
      <p><a href="{{ $listed.Permalink }}">Read more →</a></p>
    </article>
    

    One argument for doing things this way is consistency. Using .Scratch, you can make it a habit to always pass in the current page object to any partial, adding any extra data as scratch variables. Then, whenever you write or edit your partials, you know that . is a page object. Of course, you can establish a convention for yourself using a passed-in map as well: always sending along the page object as .Page, for example.

    Conclusion

    When it comes to context and data, a static site generator brings both benefits and limitations. On one hand, an operation that is too inefficient when run for every page visit may be perfectly good when run only once as the page is compiled. On the other hand, it may surprise you how often it would be useful to have access to some part of the network request even on a predominantly static site.

    To handle query string parameters, for example, on a static site, you’d have to resort to JavaScript or some proprietary solution like Netlify’s redirects. The point here is that while the jump from a dynamic to a static site is simple in theory, it does take a shift in mindset. In the beginning, it’s easy to fall back on your old habits, but practice will make perfect.

    With that, we conclude our look at data management in the Hugo static site generator. Even though we focused only on a narrow sector of its functionality, there are certainly things we didn’t cover that could have been included. Nevertheless, I hope this article gave you some added insight into how data flows from content files, to templates, to subtemplates and how it can be modified along the way.

    Note: If you already have some Hugo experience, we have a nice resource for you, quite appropriately residing on our aforementioned, Hugo-driven “Learn” site! When you just need to check the order of the arguments to the replaceRE function, how to retrieve the next page in a section, or what the “expiration date” front matter field is called, a cheat sheet comes in handy. We’ve put together just such a reference, so download a Hugo cheat sheet, in a package also featuring a host of other cheat sheets on everything from Git to the Visual Studio Code editor.

    Further Reading

    If you’re looking for more information on Hugo, here are some nice resources:

    Smashing Editorial
    (vf, il)

    Source link

    web design

    How To Migrate From WordPress To The Eleventy Static Site Generator — Smashing Magazine

    12/04/2020

    About The Author

    Scott Dawson lives in Trumansburg, New York. He’s a web designer and developer and enjoys writing, acting, creating art, and making music. Scott is a front-end …
    More about
    Scott

    If you’re a designer or developer with intermediate knowledge of HTML and JavaScript, and know your way around GitHub and the command line, this tutorial is for you. We’re going to walk step-by-step through converting a WordPress site into a static site generated from Markdown.

    Eleventy is a static site generator. We’re going to delve into why you’d want to use a static site generator, get into the nitty-gritty of converting a simple WordPress site to Eleventy, and talk about the pros and cons of managing content this way. Let’s go!

    What Is A Static Site Generator?

    I started my web development career decades ago in the mid-1990s when HTML and CSS were the only things you needed to get a website up and running. Those simple, static websites were fast and responsive. Fast forward to the present day, though, and a simple website can be pretty complicated.

    In the case of WordPress, let’s think through what it takes to render a web page. WordPress server-side PHP, running on a host’s servers, does the heavy lifting of querying a MySQL database for metadata and content, chooses the right versions of images stored on a static file system, and merges it all into a theme-based template before returning it to the browser. It’s a dynamic process for every page request, though most of the web pages I’ve seen generated by WordPress aren’t really that dynamic. Most visitors, if not all, experience identical content.

    Static site generators flip the model right back to that decades-old approach. Instead of assembling web pages dynamically, static site generators take content in the form of Markdown, merge it with templates, and create static web pages. This process happens outside of the request loop when users are browsing your site. All content has been pre-generated and is served lightning-fast upon each request. Web servers are quite literally doing what they advertise: serving. No database. No third-party plugins. Just pure HTML, CSS, JavaScript, and images. This simplified tech stack also equates to a smaller attack surface for hackers. There’s a little server-side infrastructure to exploit, so your site is inherently more secure.

    Leading static site generators are feature-rich, too, and that can make a compelling argument for bidding adieu to the tech stacks that are hallmarks of modern content management systems.

    If you’ve been in this industry for a while, you may remember Macromedia’s (pre-Adobe) Dreamweaver product. I loved the concept of library items and templates, specifically how they let me create consistency across multiple web pages. In the case of Eleventy, the concepts of templates, filters, shortcodes, and plugins are close analogs. I got started on this whole journey after reading about Smashing’s enterprise conversion to the JamStack. I also read Mathias Biilmann & Phil Hawksworth’s free book called Modern Web Development on the JAMstack and knew I was ready to roll up my sleeves and convert something of my own.

    Why Not Use A Static Site Generator?

    Static site generators require a bit of a learning curve. You’re not going to be able to easily pass off editorial functions to input content, and specific use cases may preclude you from using one. Most of the work I’ll show is done in Markdown and via the command line. That said, there are many options for using static site generators in conjunction with dynamic data, e-commerce, commenting, and rating systems.

    You don’t have to convert your entire site over all at once, either. If you have a complicated setup, you might start small and see how you feel about static site generation before putting together a plan to solve something at an enterprise scale. You can also keep using WordPress as a best-in-class headless content management system and use an SSG to serve WordPress content.

    How I Chose Eleventy As A Static Site Generator

    Do a quick search for popular static site generators and you’ll find many great options to start with: Eleventy, Gatsby, Hugo, and Jekyll were leading contenders on my list. How to choose? I did what came naturally and asked some friends. Eleventy was a clear leader in my Twitter poll, but what clinched it was a comment that said “@eleven_ty feels very approachable if one doesn’t know what one is doing.” Hey, that’s me! I can unhappily get caught up in analysis paralysis. Not today… it felt good to choose Eleventy based on a poll and a comment. Since then, I’ve converted four WordPress sites to Eleventy, using GitHub to store the code and Netlify to securely serve the files. That’s exactly what we’re going to do today, so let’s roll up our sleeves and dive in!

    Getting Started: Bootstrapping The Initial Site

    Eleventy has a great collection of starter projects. We’ll use Dan Urbanowicz’s eleventy-netlify-boilerplate as a starting point, advertised as a “template for building a simple blog website with Eleventy and deploying it to Netlify. Includes Netlify CMS and Netlify Forms.” Click “Deploy to netlify” to get started. You’ll be prompted to connect Netlify to GitHub, name your repository (I’m calling mine smashing-eleventy-dawson), and then “Save & Deploy.”

    With that done, a few things happened:

    1. The boilerplate project was added to your GitHub account.
    2. Netlify assigned a dynamic name to the project, built it, and deployed it.
    3. Netlify configured the project to use Identity (if you want to use CMS features) and Forms (a simple contact form).
    Netlify’s initial deployment screen
    This is Netlify’s screen that shows our initial deployment is completed. (Large preview)

    As the screenshot suggests, you can procure or map a domain to the project, and also secure the site with HTTPS. The latter feature was a really compelling selling point for me since my host had been charging an exorbitant fee for SSL. On Netlify, it’s free.

    I clicked Site Settings, then Change Site Name to create a more appropriate name for my site. As much as I liked jovial-goldberg-e9f7e9, elizabeth-dawson-piano is more appropriate. After all, that’s the site we’re converting! When I visit elizabeth-dawson-piano.netlify.app, I see the boilerplate content. Awesome!

    Eleventy Netlify Boilerplate with no customizations
    Our site has been built and is now ready for customizations. (Large preview)

    Let’s download the new repository to our local machine so we can start customizing the site. My GitHub repository for this project gives me the git clone command I can use in Visual Studio Code’s terminal to copy the files:

    Then we follow the remaining instructions in the boilerplate’s README file to install dependencies locally, edit the _data/metadata.json file to match the project and run the project locally.

    • npm install @11ty/eleventy
    • npm install
    • npx eleventy --serve --quiet

    With that last command, Eleventy launches the local development site at localhost:8080 and starts watching for changes.

    Preserving WordPress Posts, Pages, And Images

    The site we’re converting from is an existing WordPress site at elizabethrdawson.wordpress.com. Although the site is simple, it’d be great to leverage as much of that existing content as possible. Nobody really likes to copy and paste that much, right? WordPress makes it easy using its export function.

    WordPress Export Content screen
    WordPress lets you export content and images. (Large preview)

    Export Content gives me a zip file containing an XML extract of the site content. Export Media Library gives me a zip file of the site’s images. The site that I’ve chosen to use as a model for this exercise is a simple 3-page site, and it’s hosted on WordPress.com. If you’re self-hosting, you can go to Tools > Export to get the XML extract, but depending on your host, you may need to use FTP to download the images.

    If you open the XML file in your editor, it’s going to be of little use to you. We need a way to get individual posts into Markdown, which is the language we’re going to use with Eleventy. Lucky for us, there’s a package for converting WordPress posts and pages to Markdown. Clone that repository to your machine and put the XML file in the same directory. Your directory listing should look something like this:

    WordPress XML directory listing
    Directory listing for WordPress-export-to-markdown including WordPress’ XML file. (Large preview)

    If you want to extract posts from the XML, this will work out of the box. However, our sample site has three pages, so we need to make a small adjustment. On line 39 of parser.js, change “post” to “page” before continuing.

    Code snippet showing changes in wordpress-export-to-markdown
    Configure wordpress-export-to-markdown to export pages, not posts. (Large preview)

    Make sure you do an “npm install” in the wordpress-export-to-markdown directory, then enter “node index.js” and follow the prompts.

    That process created three files for me: welcome.md, about.md, and contact.md. In each, there’s front matter that describes the page’s title and date, and the Markdown of the content extracted from the XML. ‘Front matter’ may be a new term for you, and if you look at the section at the top of the sample .md files in the “pages” directory, you’ll see a section of data at the top of the file. Eleventy supports a variety of front matter to help customize your site, and title and date are just the beginning. In the sample pages, you’ll see this in the front matter section:

    eleventyNavigation:
      key: Home
      order: 0
    

    Using this syntax, you can have pages automatically added to the site’s navigation. I wanted to preserve this with my new pages, so I copied and pasted the content of the pages into the existing boilerplate .md files for home, contact, and about. Our sample site won’t have a blog for now, so I’m deleting the .md files from the “posts” directory, too. Now my local preview site looks like this, so we’re getting there!

    Local website preview after customizing content
    Now that we’ve customized some content, our local environment shows the current state of the site. (Large preview)

    This seems like a fine time to commit and push the updates to GitHub. A few things happen when I commit updates. Upon notification from GitHub that updates were made, Netlify runs the build and updates the live site. It’s the same process that happens locally when you’re updating and saving files: Eleventy converts the Markdown files to HTML pages. In fact, if you look in your _site directory locally, you’ll see the HTML version of your website, ready for static serving. So, as I navigate to elizabeth-dawson-piano.netlify.app shortly after committing, I see the same updates I saw locally.

    Adding Images

    We’ll use images from the original site. In the .eleventy.js file, you’ll see that static image assets should go in the static/img folder. Each page will have a hero image, and here’s where front matter works really well. In the front matter section of each page, I’ll add a reference to the hero image:

    hero: `/static/img/performance.jpg`
    

    Eleventy keeps page layouts in the _includes/layouts folder. base.njk is used by all page types, so we’ll add this code just under the navigation since that’s where we want our hero image.

    {% if (hero) %}
    <img class="page-hero" src="http://www.smashingmagazine.com/{{ hero }}" alt="Hero image for {{ title }}" />
    {% endif %}
    

    I also included an image tag for the picture of Elizabeth on the About page, using a CSS class to align it and give it proper padding. Now’s a good time to commit and see exactly what changed.

    Embedding A YouTube Player With A Plugin

    There are a few YouTube videos on the home page. Let’s use a plugin to create Youtube’s embed code automatically. eleventy-plugin-youtube-embed is a great option for this. The installation instructions are pretty clear: install the package with npm and then include it in our .eleventy.js file. Without any further changes, those YouTube URLs are transformed into embedded players. (see commit)

    Using Collections And Filters

    We don’t need a blog for this site, but we do need a way to let people know about upcoming events. Our events — for all intents and purposes — will be just like blog posts. Each has a title, a description, and a date.

    There are a few steps we need to create this new collection-based page:

    • Create a new events.md file in our pages directory.
    • Add a few events to our posts directory. I’ve added .md files for a holiday concert, a spring concert, and a fall recital.
    • Create a collection definition in .eleventy.js so we can treat these events as a collection. Here’s how the collection is defined: we gather all Markdown files in the posts directory and filter out anything that doesn’t have a location specified in the front matter.
    eleventyConfig.addCollection("events", (collection) =>
        collection.getFilteredByGlob("posts/*.md").filter( post => {
            return ( item.data.location ? post : false );
        })
    );
    
    • Add a reference to the collection to our events.md file, showing each event as an entry in a table. Here’s what iterating over a collection looks like:
    <table>
        <thead>
            <tr>
                <th>Date</th>
                <th>Title</th>
                <th>Location</th>
            </tr>    
        </thead>
        <tbody>
            {%- for post in collections.events -%}
            <tr>
                <td>{{ post.date }}</td>
                <td><a href="{{ post.url }}">{{ post.data.title }}</a></td>
                <td>{{ post.data.location }}</td>
            </tr>    
            {%- endfor -%}
        </tbody>
    </table>
    

    However, our date formatting looks pretty bad.

    Table with unformatted dates
    Our date formats could use some work. (Large preview)

    Luckily, the boilerplate .eleventy.js file already has a filter titled readableDate. It’s easy to use filters on content in Markdown files and templates:

    {{ post.date | readableDate }}

    Now, our dates are properly formatted! Eleventy’s filter documentation goes into more depth on what filters are available in the framework, and how you can add your own. (see: commit)

    Polishing The Site Design With CSS

    Okay, so now we have a pretty solid site created. We have pages, hero images, an events list, and a contact form. We’re not constrained by the choice of any theme, so we can do whatever we want with the site’s design… the sky is the limit! It’s up to you to make your site performant, responsive, and aesthetically pleasing. I made some styling and markup changes to get things to our final commit.

    Completed website
    Our website conversion is complete. (Large preview)

    Now we can tell the world about all of our hard work. Let’s publish this site.

    Publishing The Site

    Oh, but wait. It’s already published! We’ve been working in this nice workflow all along, where our updates to GitHub automatically propagate to Netlify and get rebuilt into fresh, fast HTML. Updates are as easy as a git push. Netlify detects the changes from git, processes markdown into HTML, and serves the static site. When you’re done and ready for a custom domain, Netlify lets you use your existing domain for free. Visit Site Settings > Domain Management for all the details, including how you can leverage Netlify’s free HTTPS certificate with your custom domain.

    Advanced: Images, Contact Forms, And Content Management

    This was a simple site with only a few images. You may have a more complicated site, though. Netlify’s Large Media service allows you to upload full-resolution images to GitHub, and stores a pointer to the image in Large Media. That way, your GitHub repository is not jam-packed with image data, and you can easily add markup to your site to request optimized crops and sizes of images at request time. I tried this on my own larger sites and was really happy with the responsiveness and ease of setup.

    Remember that contact form that was installed with our boilerplate? It just works. When you submit the contact form, you’ll see submissions in Netlify’s administration section. Select “Forms” for your site. You can configure Netlify to email you when you get a new form submission, and you can also add a custom confirmation page in your form’s code. Create a page in your site at /contact/success, for example, and then within your form tag (in form.njk), add action="/contact/success" to redirect users there once the form has been submitted.

    The boilerplate also configures the site to be used with Netlify’s content manager. Configuring this to work well for a non-technical person is beyond the scope of the article, but you can define templates and have updates made in Netlify’s content manager sync back to GitHub and trigger automatic redeploys of your site. If you’re comfortable with the workflow of making updates in markdown and pushing them to GitHub, though, this capability is likely something you don’t need.

    Further Reading

    Here are some links to resources used throughout this tutorial, and some other more advanced concepts if you want to dive deeper.

    Smashing Editorial
    (ra, yk, il)

    Source link

    web design

    Simplify Your Stack With A Custom-Made Static Site Generator — Smashing Magazine

    09/23/2020

    About The Author

    Bryan is a designer, developer, and educator with a passion for CSS and static sites. He actively works to mentor and teach developers and designers the value …
    More about
    Bryan
    Robinson

    In modern development, there are so many great tools for developing websites, but often they are more than what’s necessary for a given project. In this article, we’ll explore how to take a humble HTML page and make its content editable in a CMS with no frameworks and no client-side JavaScript.

    With the advent of the Jamstack movement, statically-served sites have become all the rage again. Most developers serving static HTML aren’t authoring native HTML. To have a solid developer experience, we often turn to tools called Static Site Generators (SSG).

    These tools come with many features that make authoring large-scale static sites pleasant. Whether they provide simple hooks into third-party APIs like Gatsby’s data sources or provide in-depth configuration like 11ty’s huge collection of template engines, there’s something for everyone in static site generation.

    Because these tools are built for diverse use cases, they have to have a lot of features. Those features make them powerful. They also make them quite complex and opaque for new developers. In this article, we’ll take the SSG down to its basic components and create our very own.

    What Is A Static Site Generator?

    At its core, a static site generator is a program that performs a series of transformations on a group of files to convert them into static assets, such as HTML. What sort of files it can accept, how it transforms them, and what types of files come out differentiate SSGs.

    Jekyll, an early and still popular SSG, uses Ruby to process Liquid templates and Markdown content files into HTML.

    Gatsby uses React and JSX to transform components and content into HTML. It then goes a step further and creates a single-page application that can be served statically.

    11ty renders HTML from templating engines such as Liquid, Handlebars, Nunjucks, or JavaScript template literals.

    Each of these platforms has additional features to make our lives easier. They provide theming, build pipelines, plugin architecture, and more. With each additional feature comes more complexity, more magic, and more dependencies. They’re important features, to be sure, but not every project needs them.

    Between these three different SSGs, we can see another common theme: data + templates = final site. This seems to be the core functionality of generator static sites. This is the functionality we’ll base our SSG around.

    At its core, a static site generator is a program that performs a series of transformations on a group of files to convert them into static assets, such as HTML.

    Our New Static Site Generator’s Technology Stack: Handlebars, Sanity.io And Netlify

    To build our SSG, we’ll need a template engine, a data source, and a host that can run our SSG and build our site. Many generators use Markdown as a data source, but what if we took it a step further and natively connected our SSG to a CMS?

    • Data Source: Sanity.io
    • Data fetching and templating: Node and Handlebars
    • Host and Deployment: Netlify.

    Prerequisites

    • NodeJS installed
    • Sanity.io account
    • Knowledge of Git
    • Basic knowledge of command line
    • Basic knowledge of deployment to services like Netlify.

    Note: To follow along, you can find the code in this repository on GitHub.

    Setting Up Our Document Structure In HTML

    To start our document structure, we’re going to write plain HTML. No need to complicate matters yet.

    In our project structure, we need to create a place for our source files to live. In this case, we’ll create a src directory and put our index.html inside.

    In index.html, we’ll outline the content we want. This will be a relatively simple about page.

    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Title of the page!</title>
    </head>
    <body>
        <h1>The personal homepage of Bryan Robinson</h1>
    
        <p>Some pagraph and rich text content next</p>
    
        <h2>Bryan is on the internet</h2>
        <ul>
            <li><a href="linkURL">List of links</a></li>
        </ul>
    </body>
    </html>

    Let’s keep this simple. We’ll start with an h1 for our page. We’ll follow that with a few paragraphs of biographical information, and we’ll anchor the page with a list of links to see more.

    Convert Our HTML Into A Template That Accepts Data

    After we have our basic structure, we need to set up a process to combine this with some amount of data. To do this, we’ll use the Handlebars template engine.

    At its core, Handlebars takes an HTML-like string, inserts data via rules defined in the document, and then outputs a compiled HTML string.

    To use Handlebars, we’ll need to initialize a package.json and install the package.

    Run npm init -y to create the structure of a package.json file with some default content. Once we have this, we can install Handlebars.

    npm install handlebars

    Our build script will be a Node script. This is the script we’ll use locally to build, but also what our deployment vendor and host will use to build our HTML for the live site.

    To start our script, we’ll create an index.js file and require two packages at the top. The first is Handlebars and the second is a default module in Node for accessing the current file system.

    const fs = require('fs');
    const Handlebars = require('handlebars');

    We’ll use the fs module to access our source file, as well as to write to a distribution file. To start our build, we’ll create a main function for our file to run when called and a buildHTML function to combine our data and markup.

    function buildHTML(filename, data) {
      const source = fs.readFileSync(filename,'utf8').toString();
      const template = Handlebars.compile(source);
      const output = template(data);
    
      return output
    }
    
    async function main(src, dist) {
      const html = buildHTML(src, { "variableData": "This is variable data"});
     
      fs.writeFile(destination, html, function (err) {
        if (err) return console.log(err);
          console.log('index.html created');
      });
    }
    
    main('./src/index.html', './dist/index.html');

    The main() function accepts two arguments: the path to our HTML template and the path we want our built file to live. In our main function, we run buildHTML on the template source path with some amount of data.

    The build function converts the source document into a string and passes that string to Handlebars. Handlebars compiles a template using that string. We then pass our data into the compiled template, and Handlebars renders a new HTML string replacing any variables or template logic with the data output.

    We return that string into our main function and use the writeFile method provided by Node’s file-system module to write the new file in our specified location if the directory exists.

    To prevent an error, add a dist directory into your project with a .gitkeep file in it. We don’t want to commit our built files (our build process will do this), but we’ll want to make sure to have this directory for our script.

    Before we create a CMS to manage this page, let’s confirm it’s working. To test, we’ll modify our HTML document to use the data we just passed into it. We’ll use the Handlebars variable syntax to include the variableData content.

    <h1>{{ variableData }}</h1>

    Now that our HTML has a variable, we’re ready to run our node script.

    node index.js

    Once the script finishes, we should have a file at /dist/index.html. If we read open this in a browser, we’ll see our markup rendered, but also our “This is variable data” string, as well.

    Connecting To A CMS

    We have a way of putting data together with a template, now we need a source for our data. This method will work with any data source that has an API. For this demo, we’ll use Sanity.io.

    Sanity is an API-first data source that treats content as structured data. They have an open-source content management system to make managing and adding data more convenient for both editors and developers. The CMS is what’s often referred to as a “Headless” CMS. Instead of a traditional management system where your data is tightly coupled to your presentation, a headless CMS creates a data layer that can be consumed by any frontend or service (and possibly many at the same time).

    Sanity is a paid service, but they have a “Standard” plan that is free and has all the features we need for a site like this.

    Setting Up Sanity

    The quickest way to get up and running with a new Sanity project is to use the Sanity CLI. We’ll start by installing that globally.

    npm install -g @sanity/cli

    The CLI gives us access to a group of helpers for managing, deploying, and creating. To get things started, we’ll run sanity init. This will run us through a questionnaire to help bootstrap our Studio (what Sanity calls their open-source CMS).

    Select a Project to Use:
       Create new project
       HTML CMS
    
    Use the default dataset configuration?   
       Y // this creates a "Production" dataset
    
    Project output path:
       studio // or whatever directory you'd like this to live in
    
    Select project template
       Clean project with no predefined schemas

    This step will create a new project and dataset in your Sanity account, create a local version of Studio, and tie the data and CMS together for you. By default, the studio directory will be created in the root of our project. In larger-scale projects, you may want to set this up as a separate repository. For this project, it’s fine to keep this tied together.

    To run our Studio locally, we’ll change the directory into the studio directory and run sanity start. This will run Studio at localhost:3333. When you log in, you’ll be presented with a screen to let you know you have “Empty schema.” With that, it’s time to add our schema, which is how our data will be structured and edited.

    Creating Sanity Schema

    The way you create documents and fields within Sanity Studio is to create schemas within the schemas/schema.js file.

    For our site, we’ll create a schema type called “About Details.” Our schema will flow from our HTML. In general, we could make most of our webpage a single rich-text field, but it’s a best practice to structure our content in a de-coupled way. This provides greater flexibility in how we might want to use this data in the future.

    For our webpage, we want a set of data that includes the following:

    • Title
    • Full Name
    • Biography (with rich text editing)
    • A list of websites with a name and URL.

    To define this in our schema, we create an object for our document and define out its fields. An annotated list of our content with its field type:

    • Title — string
    • Full Name — string
    • Biography — array of “blocks”
    • Website list — array of objects with name and URL string fields.
    types: schemaTypes.concat([
        /* Your types here! */
    
        {
            title: "About Details",
            name: "about",
            type: "document",
            fields: [
                {
                    name: 'title',
                    type: 'string'
                },
                {
                    name: 'fullName',
                    title: 'Full Name',
                    type: 'string'
                },
                {
                    name: 'bio',
                    title: 'Biography',
                    name: 'content',
                    type: 'array',
                    of: [
                        {
                            type: 'block'
                        }
                    ]
                },
                {
                    name: 'externalLinks',
                    title: 'Social media and external links',
                    type: 'array',
                    of: [
                        {
                            type: 'object',
                            fields: [
                                { name: 'text', title: 'Link text', type: 'string' },
                                { name: 'href', title: 'Link url', type: 'string' }
                            ]
                        }
                    ]
                }
            ]
        }
    ])

    Add this to your schema types, save and your Studio will recompile and present you with your first documents. From here, we’ll add our content into the CMS by creating a new document and filling out the information.

    Structuring Your Content In A Reusable Way

    At this point, you may be wondering why we have a “full name” and a “title.” This is because we want our content to have the potential to be multipurpose. By including a name field instead of including the name just in the title, we give that data more use. We can then use information in this CMS to also power a resumé page or PDF. The biography field could be programmatically used in other systems or websites. This allows us to have a single source of truth for much of this content instead of being dictated by the direct use case of this particular site.

    Pulling Our Data Into Our Project

    Now that we’ve made our data available via an API, let’s pull it into our project.

    Install and configure the Sanity JavaScript client

    First thing, we need access to the data in Node. We can use the Sanity JavaScript client to forge that connection.

    npm install @sanity/client

    This will fetch and install the JavaScript SDK. From here, we need to configure it to fetch data from the project we set up earlier. To do that, we’ll set up a utility script in /utils/SanityClient.js. We provide the SDK with our project ID and dataset name, and we’re ready to use it in our main script.

    const sanityClient = require('@sanity/client');
    const client = sanityClient({
        projectId: '4fs6x5jg',
        dataset: 'production',
        useCdn: true 
      })
    
    module.exports = client;

    Fetching Our Data With GROQ

    Back in our index.js file, we’ll create a new function to fetch our data. To do this, we’ll use Sanity’s native query language, the open-source GROQ.

    We’ll build the query in a variable and then use the client that we configured to fetch the data based on the query. In this case, we build an object with a property called about. In this object, we want to return the data for our specific document. To do that, we query based on the document _id which is generated automatically when we create our document.

    To find the document’s _id, we navigate to the document in Studio and either copy it from the URL or move into “Inspect” mode to view all the data on the document. To enter Inspect, either click the “kabob” menu at the top-right or use the shortcut Ctrl + Alt + I. This view will list out all the data on this document, including our _id. Sanity will return an array of document objects, so for simplicity’s sake, we’ll return the 0th entry.

    We then pass the query to the fetch method of our Sanity client and it will return a JSON object of all the data in our document. In this demo, returning all the data isn’t a big deal. For bigger implementations, GROQ allows for an optional “projection” to only return the explicit fields you want.

    const client = require('./utils/SanityClient') // at the top of the file
    
    // ...
    
    async function getSanityData() {
        const query = `{
            "about": *[_id == 'YOUR-ID-HERE'][0]
        }`
        let data = await client.fetch(query);
    }

    Converting The Rich Text Field To HTML

    Before we can return the data, we need to do a transformation on our rich text field. While many CMSs use rich text editors that return HTML directly, Sanity uses an open-source specification called Portable Text. Portable Text returns an array of objects (think of rich text as a list of paragraphs and other media blocks) with all the data about the rich text styling and properties like links, footnotes, and other annotations. This allows for your text to be moved and used in systems that don’t support HTML, like voice assistants and native apps.

    For our use case, it means we need to transform the object into HTML. There are NPM modules that can be used to convert portable text into various uses. In our case we’ll use a package called block-content-to-html.

    npm install @sanity/block-content-to-html

    This package will render all the default markup from the rich text editor. Each type of style can be overridden to conform to whatever markup you need for your use case. In this case, we’ll let the package do the work for us.

    const blocksToHtml = require('@sanity/block-content-to-html'); // Added to the top
    
    async function getSanityData() {
        const query = `{
            "about": *[_type == 'about'][0]
        }`
        let data = await client.fetch(query);
        data.about.content = blocksToHtml({
            blocks: data.about.content
        })
        return await data
    }

    Using The Content From Sanity.io In Handlebars

    Now that the data is in a shape we can use it, we’ll pass this to our buildHTML function as the data argument.

    async function main(src, dist) {
        const data = await getSanityData();
        const html = buildHTML(src, data)
    
        fs.writeFile(dist, html, function (err) {
            if (err) return console.log(err);
            console.log('index.html created');
        });
    }

    Now, we can change our HTML to use the new data. We’ll use more variable calls in our template to pull most of our data.

    To render our rich text content variable, we’ll need to add an extra layer of braces to our variable. This will tell Handlebars to render the HTML instead of displaying the HTML as a string.

    For our externalLinks array, we’ll need to use Handlebars’ built-in looping functionality to display all the links we added to our Studio.

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>{{ about.title }}</title>
    </head>
    <body>
        <h1>The personal homepage of {{ about.fullName }}</h1>
    
        {{{ about.content }}}
    
        <h2>Bryan is on the internet</h2>
        <ul>
            {{#each about.externalLinks }}
                <li><a href="{{ this.href }}">{{ this.text }}</a></li>
            {{/each}}
        </ul>
    </body>
    </html>

    Setting Up Deployment

    Let’s get this live. We need two components to make this work. First, we want a static host that will build our files for us. Next, we need to trigger a new build of our site when content is changed in our CMS.

    Deploying To Netlify

    For hosting, we’ll use Netlify. Netlify is a static site host. It serves static assets, but has additional features that will make our site work smoothly. They have a built-in deployment infrastructure that can run our node script, webhooks to trigger builds, and a globally distributed CDN to make sure our HTML page is served quickly.

    Netlify can watch our repository on GitHub and create a build based on a command that we can add in their dashboard.

    First, we’ll need to push this code to GitHub. Then, in Netlify’s Dashboard, we need to connect the new repository to a new site in Netlify.

    Once that’s hooked up, we need to tell Netlify how to build our project. In the dashboard, we’ll head to Settings > Build & Deploy > Build Settings. In this area, we need to change our “Build command” to “node index.js” and our “Publish directory” to “./dist”.

    When Netlify builds our site, it will run our command and then check the folder we list for content and publish the content inside.

    Setting Up A Webhook

    We also need to tell Netlify to publish a new version when someone updates content. To do that, we’ll set up a Webhook to notify Netlify that we need the site to rebuild. A Webhook is a URL that can be programmatically accessed by a different service (such as Sanity) to create an action in the origin service (in this case Netlify).

    We can set up a specific “Build hook” in our Netlify dashboard at Settings > Build & Deploy > Build hooks. Add a hook, give it a name and save. This will provide a URL that can be used to remotely trigger a build in Netlify.

    Next, we need to tell Sanity to make a request to this URL when you publish changes.

    We can use the Sanity CLI to accomplish this. Inside of our /studio directory, we can run sanity hook create to connect. The command will ask for a name, a dataset, and a URL. The name can be whatever you’d like, the dataset should be production for our product, and the URL should be the URL that Netlify provided.

    Now, whenever we publish content in Studio, our website will automatically be updated. No framework necessary.

    Next Steps

    This is a very small example of what you can do when you create your own tooling. While more full-featured SSGs may be what you need for most projects, creating your own mini-SSG can help you understand more about what’s happening in your generator of choice.

    • This site publishes only one page, but with a little extra in our build script, we could have it publish more pages. It could even publish a blog post.
    • The “Developer experience” is a little lacking in the repository. We could run our Node script on any file saves by implementing a package like Nodemon or add “hot reloading” with something like BrowserSync.
    • The data that lives in Sanity can power multiple sites and services. You could create a resumé site that uses this and publishes a PDF instead of a webpage.
    • You could add CSS and make this look like a real site.
    Smashing Editorial
    (ra, yk, il)

    Source link