Lots of web software is configured to create and serve
web files/pages with an .html
extension/suffix.
That includes
11ty,
which by default creates an index.html
for each
content template. It includes
Browsersync
— the hot-reload server invoked when you run
npx @11ty/eleventy --serve
— which
determines the Content-Type
response header based
on the output file's extension. And it includes
Apache HTTP server, which,
like Browsersync, uses the extension to map a file
to a Content-Type
header.
And yet, even if your software defaults to .html
, it
is not mandatory for the web.
There is no requirement that certain characters be
attached to your web page
urls.
In this article,
I'll explain how to make
clean urls
with Apache, Browsersync, and 11ty.
apache
Suppose you have a server running Apache at
http://example.com
, you use static files to store web
pages, and one of those files is called foo.html
.
If a browser requests http://example.com/foo.html
,
Apache will include the following header with the page:
Content-Type: text/html; charset=utf-8
A few things to note:
- the header determines the content type
- the url does not
- the last part of the url (
.html
) is not a file extension, even if it sort of looks like one
That means you can remove the url suffix if you want to, but first you must change how Apache handles extensionless files. In your Apache configuration, add a FilesMatch section with a regular expression as follows:
<FilesMatch "^[^.]+$">
</FilesMatch>
Any configurations inside that FilesMatch
block will apply
only to files that conform to the regular expression. In this
case, the regular expression specifies no dot .
character
in the name, so the file foo
matches, but files such as
foo.jpeg
, foo.js
, etc. do not.
Next we add a ForceType directive inside that block:
<FilesMatch "^[^.]+$">
ForceType 'text/html; charset=utf-8'
</FilesMatch>
ForceType
, as its name suggests, forces Apache to
treat all files as type text/html
. But since it's in a
FilesMatch
block, the directive will only apply to
extensionless files. Now, the url
http://example.com/foo
will be sent as text/html
,
the same if it were
http://example.com/foo.html
.
11ty
By default, 11ty sort of creates clean urls.
Given the source file foo.md
:
---
title: Foo
---
Lorem ipsum dolor sit amet, consectetur adipiscing elit....
11ty makes a directory named /foo
/ and writes the page
content to index.html
inside that directory. And it links
to the page using the directory as the address:
<a href="/foo/">Foo</a>
This works because servers like Apache will accept a request
for /foo/
, find the directory /foo/
, and return the
default file,
which is typically index.html
.
So 11ty's default url is almost clean, but there's still
an extraneous trailing slash /
at the end.
In addition, having every page output as index.html
can
make working with your output files difficult. Suppose you
wanted to compare the output of content templates named
foo.md
, bar.md
, and bat.md
. You would have to examine
three different files all named index.html
.
Distinguishing one from the others could be tricky.

index.html
To change this default, set the permalink in the file's front matter:
---
title: Foo
peramlink: foo
---
Lorem ipsum dolor sit amet, consectetur adipiscing elit....
Now 11ty will write foo
instead of foo/index.html
, and
make links using /foo
instead of /foo/
. And we already got
Apache to serve /foo
as text/html
. In principle, all is well.
But...
Browsersync (11ty's server)
...there's one more problem.
If you use the --serve
switch to run the
11ty test server
and you try to navigate to your Foo page, the server sends
the wrong header:
Content-Type: application/octet-stream
And your browser, instead of displaying Foo, prompts you to download it. That makes the hot-reload test server all but useless. So our last task is to configure that server, called Browsersync, to mimic the way Apache behaves (assuming you've changed your Apache config as shown in the first part of this article).
In your
.eleventy.js
configuration file,
add an eleventyConfig.setBrowserSyncConfig
function:
module.exports = function(eleventyConfig) {
// other config directives that you already have,
// like addFilter, addWatchTarget, etc.
eleventyConfig.setBrowserSyncConfig({
middleware: [
function (req, res, next) {
if (/^[^.]+$/.test(req.url)) {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
}
next();
}
]
});
// my 11ty directories; N.B. yours might be different!
return {
dir: {
input: 'src',
layouts: '_templates',
output: 'public_html'
}
};
};
The new function tests the browser req
, that is, the
request from the browser, using a regular expression.
You may have noted that the regular expression being
tested against, ^[^.]+$
, is the same as the one
in the Apache FilesMatch
tag in the first part of
this article. It works the same way: if the url does
not contain a dot .
character, Browsersync sends this
header:
Content-Type: text/html; charset=utf-8
Now the hot-reload server is working, Apache is working, and you have “extensionless” urls with 11ty.
Thanks to
GitHub
user
Paul Shryock for the
setBrowserSyncConfig
idea.