Possum websites, done possumly

Part 10: A bit of polish

11 minute read time / 1435 words

In this final part, let's look at adding a few niceties, improvements and accessibility additions to what we've created so far.

Recap

We've covered all the basics (and a few more advanced topics too), and we've now got a site that has textpages, articles with tagging, article and category listing pages, a sitemap, an RSS feed, data-powered social media links, and some nice custom macros and filters to help us on our way.

Our current global navigation is pretty basic. It just loops through the pages it's given and outputs a link to each page in a list. We can improve this in two simple ways:

  1. Add an 'active' class for the current page so that when we come to add our CSS, we can make the active page look active.
  2. Add the aria-current="page" attribute and value to help assistive software understand our active page and context within the site.

Our navItem macro currently looks like this:

{% macro navItem(entry) %}
<li>
    <a href="{{ entry.url }}">
        {{ entry.title }}
    </a>
</li>
{% endmacro %}

We can set a new variable to check if the current page URL is contained in the URL of the item the menu is outputting by using our custom contains filter:

{% macro navItem(entry, page) %}
    {% set activePage = true | contains(page.url, [entry.url]) %}

    <li>
        <a
            href="{{ entry.url }}"
            class="link{% if activePage %} is_active{% endif %}"
            {% if activePage %}aria-current="page"{% endif %}
        >
            {{ entry.title }}
        </a>
    </li>
{% endmacro %}

To make it work, we've had to add the current page variable into the mix, so now we need to pass that from our nav.njk include too. We just need to update the call to our navItem macro and add the page variable to it:

{%- for entry in pages %}
    {{ navItem(entry, page) }}
{% endfor %}

This will, unfortunately, always set the 'home' link to active because '/' is always going to be part of every URL on the site. On the plus side though, it means we can just remove it from our navigation, because our site name is already a link to the homepage, and that's expected behaviour.

We can just go ahead and remove the eleventyNavigation Frontmatter metadata from our home page.

Base styles

From an Eleventy point of view, we've got a fairly complete, working static site. But it's got zero style at the moment. There's a million different ways you could probably add style to these pages, but I like to use a basic Gulp task runner set up that processes source CSS files and compiles, transforms and minifies out to a single, sensible production-ready stylesheet.

We're going to need a bunch of NPM packages installed to get this working, so let's start there:

Create a new file called package.json in the root folder of the site with the following content:

{
  "name": "mysexyeleventysite",
  "version": "1.0.0",
  "description": "",
  "main": "Gulpfile.js",
  "scripts": {
    "prod": "concurrently \"npx gulp --production\" \"npx eleventy\"",
    "dev": "concurrently \"npx @11ty/eleventy --serve\" \"npx gulp dev\""
  },
  "browserslist": [
    "last 2 major versions"
  ],
  "devDependencies": {
    "@11ty/eleventy": "^2.0.1",
    "@11ty/eleventy-navigation": "^0.3.5",
    "@11ty/eleventy-plugin-rss": "^1.2.0",
    "@babel/cli": "^7.23.4",
    "@babel/core": "^7.23.7",
    "@babel/register": "^7.23.7",
    "@babel/runtime-corejs3": "^7.23.7",
    "autoprefixer": "^10.4.16",
    "concurrently": "^8.2.2",
    "gulp": "^4.0.2",
    "gulp-babel": "^8.0.0",
    "gulp-postcss": "^9.0.1",
    "gulp-rename": "^2.0.0",
    "html-minifier": "^4.0.0",
    "postcss": "^8.4.33",
    "postcss-import": "^16.0.0",
    "postcss-lightningcss": "^1.0.0",
    "postcss-nested": "^6.0.1",
    "postcss-preset-env": "^9.3.0",
    "postcss-reporter": "^7.0.5",
    "postcss-scss": "^4.0.9",
    "postcss-variable-compress": "^3.0.0"
  }
}

This file is used to tell NPM what packages we want and at what versions, but it also lets us set up node scripts and set up our browserslist so any clever CSS plugins we use will know what browsers we want to support.

This does nothing on its own though, and if we want to install all these packages, we need to tell NPM to do just that. In the root folder of the site run:

npm i

Now we need to set up our CSS tasks in Gulp. Create a new file in the root of the site called gulpfile.js and put this code in it:

const gulp = require("gulp");

const postcss = require("gulp-postcss");
const rename = require("gulp-rename");
const lightningCss = require("postcss-lightningcss");
const nested = require("postcss-nested");
const partialImports = require("postcss-import");
const presetEnv = require("postcss-preset-env");
const reporter = require("postcss-reporter");
const variableCompress = require("postcss-variable-compress");

function watchCss(done) {
    const cssFiles = 'src/css/**/*.css';
    const watcher = gulp.watch(cssFiles);

    watcher.on("change", function (path, stats) {
        console.log(`Watcher fired for: ${path}`);

        postCss();
    });

    done();
}

function postCss() {
    const entryFilename = "src/css/index.css";
    const outputFilename = "site.css";
    const outputDest = "dist/css";

    return gulp
        .src(entryFilename, { sourcemaps: true })
        .pipe(
            postcss([
                partialImports,
                presetEnv({
                    features: {
                        "cascade-layers": false,
                    },
                }),
                nested,
                lightningCss,
                variableCompress,
                reporter({
                    clearReportedMessages: true,
                    clearAllMessages: true,
                    throwError: false,
                    positionless: "last",
                }),
            ]),
        )
        .pipe(rename(outputFilename))
        .pipe(
            gulp.dest(outputDest, { sourcemaps: "." })
        )
    ;
}

gulp.task("default", postCss);
gulp.task("dev", gulp.series(postCss, watchCss));

I'll not go through what each and every item in here does, but suffice to say this is enough to give us a basic PostCSS set up which will consume source CSS files and automatically check if any of the modern CSS we've written needs a bit extra help for any browser we're supporting or not. It will concatenate all our partial style files and it will allow us to nest our CSS too.

Setting up Gulp tasks properly and finding all the plugins you might need is a whole other volume of documentation altogether!

There's a 'watch' task as well so that when we're running locally we can keep an eye on our source CSS file and automatically run the compilation step as soon as any new file changes are saved.

Now that we have our package file, our tasks file and we've installed our new packages too, we need to add a few things before we can get our CSS compiling. First of all, create a new folder at /src/css/ and create a file in here called index.css with a few basic styles in (just so we can see that it works!):

body {
    font-size: 16px;
    margin: 0;
    font-family: system-ui;
}

Now we need to make sure our base templates are going to pull our new CSS file in when it's been compiled. Open up our head include and add the following to it:

<link rel="stylesheet" href="{{ '/css/site.css' | url }}">
<link rel="preload" href="{{ '/css/site.css' | url }}" as="style">

If you now go to your terminal and run the development command: npm run dev instead of our previous command npx @11ty/eleventy --serve you should get both your Eleventy site build as well as your new stylesheet compiling. Go to the site and refresh and you should at least see that the browser default serif font has been replaced most likely with a sans serif fontface. Success!

Social sharing metadata

We can give Facebook, Twitter, Pinterest and LinkedIn (at least!) a bit of a helping hand with how our pages will look when shared on different platforms. First we'll need to add the right meta elements into our head include:

<meta property="og:site_name" content="{{ settings.sitename }}">
<meta property="og:title" content="{{ title }}">
<meta property="og:description" content="{{ description }}">
<meta property="og:type" content="website">
<meta property="og:image" content="{{ settings.domain }}/img/social/sharer.jpg">
<meta property="og:image:width" content="1280">
<meta property="og:image:height" content="800">
<meta property="og:image:alt" content="{{ description }}" />
<meta property="og:url" content="{{ settings.domain }}{{ page.url }}">
<meta property="article:published_time" content="{{ settings.lastupdated }}" />
<meta property="article:author" content="{{ settings.author }}" />
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="@{{ settings.xhandle }}">
<meta name="twitter:creator" content="@{{ settings.xhandle }}">

You might notice in there that we're pulling our domain name, sitename and our X handle from our settings.json global data file. You'll need to get an image and place it at /public/img/social/sharer.jpg too.

We can also update our footer's copyright message with the current year at build time with a nice little addition to our footer include and our Eleventy config file:

eleventyConfig.addShortcode('year', () => `${new Date().getFullYear()}`);

And in our footer file:

<footer>
    &copy; 1982 - {% year %}. Me! Because I made this.

    {% include 'social.njk' %}
</footer>

What else?

This is only scraping the surface of what Eleventy can do. There are other plugins to help expand your functionality out there, and you can always roll your own too.

If you're not a superfan of Nunjucks (not sure why that would ever happen, it's lovely!) then there's also plenty of choice of other ways to template for Eleventy too.