andrew

package module
v0.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 1, 2025 License: MIT Imports: 21 Imported by: 0

README

andrew

Andrew is a web server like web servers used to be™.

  • It renders web pages from the file system, no databases involved. This is the basic design restriction that informs feature decisions.
  • You write html files, css files and javascript, in a standard directory hierarchy. No opinions about frameworks, Andrew's just a web server.
  • You get started by writing an "index.html" file and then running Andrew from that directory.
  • You can optionally provide an ssl certificate and key. No need for nginx or something to front Andrew.
  • Convenient support for creating lists of files from the current directory down, so you don't need to maintain that list by hand. You control the specific order of the pages in the list by simply providing an html meta tag in your <head> element
  • Sitemap automatically generated from your file system layout, as search engines like to see this for brand new websites.
  • RSS Feed automatically generated from your file system layout. Folks can subscript at baseUrl/rss.xml

I wanted an http server that allows me to add a simple go template instruction into an index.html that is replaced with the contents of any html files that are below the current index.html in the file system hierarchy.

Andrew contains the concept of an AndrewPage. This structure makes various pieces of metadata stored within your web page available to Andrew for creating links and sorting pages in the various tables of contents available (see below). The specifics are explained below, but conceptually I'm trying to use standard html elements to inform Andrew about site metadata. For more you may want to check the Architecture.md

To run it

go run github.com/playtechnique/andrew/cmd/andrew@latest is the simple way. The available versions are all git tags.

There are github releases, too. You can find compiled binaries at https://github.com/PlayTechnique/andrew/releases for linux, windows, and macOS for both amd64 and arm64 on all systems.

Here's how I install andrew through Docker:

FROM golang:1.23 AS base

WORKDIR /usr/src/app

ENV CGO_ENABLED=0
RUN go install github.com/playtechnique/andrew/cmd/andrew@latest

FROM scratch

COPY --from=base /etc/passwd /etc/passwd
USER 1000
COPY --from=base /go/bin/andrew /andrew
COPY --chown=1000:1000 content /website

EXPOSE 8080

ENTRYPOINT ["/andrew"]

Arguments and Options

arguments are mandatory. Options aren't.

Arguments

andrew accepts up to three arguments, in this order:

andrew [contentRoot] [address] [baseUrl]

contentRoot is the directory you're serving from, that contains your top level index.html. andrew follows apache's lead on expecting index.html in any directory as a default page.

address is the address you want to bind the server to. Specify as an address:port combination.

baseUrl is the hostname you're serving from. This is a part of sitemaps and future rss feeds. It also contains the protocol e.g. https://playtechnique.io

Options

-h | --help - show the help

-c | --cert - this is a paired option with the option below. The path to an SSL cert bundle.

-p |--privatekey - this is paired with the option above. The path to your ssl private key.

-d |--rssdescription - a short description of your RSS feed.

-t |--rsstitle - the title for your RSS feed.

Feature Specifics

SSL Support

Want to serve your site over https? So does everyone else! Start up andrew with the arguments --cert and --privatekey. If you forget one of them, but supply the other, you'll get a helpful error reminding you what you need to do. Andrew happily serves over https. It also serves over http.

Andrew's Custom Page Elements

Valid Go Template Instructions for Rendering Page Structures
.AndrewTableOfContents
.AndrewTableOfContentsWithDirectories

These are for generating lists of web pages that exist at the same level in the file system as the web page and in child directories.

Note that each of these creates its items inside a div. Here's your cheat sheet:

.AndrewTableOfContents becomes a div with class AndrewTableOfContents
.AndrewTableOfContentsWithDirectories becomes a div with class AndrewTableOfContentsWithDirectories

Andrew sorts by page publish date. This publish date is tricky for a file-based web server to get consistent, so here's the rules:

  1. If you have the tag <meta name="andrew-publish-time" content="YYYY-MM-DD"/>, Andrew uses this date.
  2. If you have the tag <meta name="andrew-publish-time" content="YYYY-MM-DD HH:MM:SS"/>, Andrew refines the date with the time published. This allows you to publish several articles on the same day and get the ordering correct..
  3. Andrew uses the page's mtime. This means that if you edit a page that does not contain the andrew-publish-time element, then you will push it back to the top of the list. This is the worst solution if you're using andrew in a container.

If you want to automate generating the datestamp, this'll get you where you want to be on macOS or linux date +"%Y-%m-%d %H:%M:%S"

Semantically Meaningful Andrew-specific HTML elements
<meta name="andrew-publish-time" content="YYYY-MM-DD"/>
<title>Your page title</title>

All meta elements are actually parsed in the Andrew Page, but Andrew doesn't use a lot of them just yet.

Custom CSS IDs and classes

I've tried to consistently include the string andrew in front of any CSS classes or IDs, so they're less likely to clash with your whimsy for laying out your own site.

The reason these classes and IDs exist is simple: it makes it easier for you to style Andrew's unstyled HTML. I don't want Andrew making decisions about your website's layout.

I include classes and IDs that get my sites looking how I want. If you need more, file a request.

How does the .AndrewTableOfContents render?

AndrewTableOfContents is for rendering a table of contents of the pages beneath the current page. It only lists page links. If you want your links grouped by directories, check out .AndrewTableOfContentsWithDirectories.

This is handled in linksbuilder.go. I try to keep this README up to date, but if seems like it doesn't sync with reality the final word is the source code.

Given this file system structure:

index.html
articles/
        index.html
        article-1.html
        article-2.html
        article-2.css
        article-1.js
fanfics/
        index.html
        story-1/
                potter-and-draco.html
        story-2/
                what-if-elves-rode-mice-pt1.html
                what-if-elves-rode-mice-pt2.html

if articles/index.html contains {{ .AndrewTableOfContents }} anywhere, that will be replaced with a div like this one:

    <div class="AndrewTableOfContents">
    <ul>
    <li><a class="andrewtableofcontentslink" id="andrewtableofcontentslink0" href="article-1.html">article 1</a>  - <span class=\"publish-date\">0000-00-01</span></li>
    <li><a class="andrewtableofcontentslink" id="andrewtableofcontentslink1" href="article-2.html">article 2</a>  - <span class=\"publish-date\">0000-00-01</span></li>
    </ul>
    </div>

if fanfics/index.html contains {{ .AndrewTableOfContents }}, that'll be replaced with a div like this one:

    <div class="AndrewTableOfContents">
    <ul>
    <li><a class="andrewtableofcontentslink" id="andrewtableofcontentslink0" href="story-1/potter-and-draco.html">Potter and Draco</a>  - <span class=\"publish-date\">0000-00-01</span></li>
    <li><a class="andrewtableofcontentslink" id="andrewtableofcontentslink1" href="story-2/what-if-elves-rode-mice-pt1.html">what-if-elves-rode-mice-pt1.html</a>  - <span class=\"andrew-page-publish-date\">0000-00-01</span></li>
    <li><a class="andrewtableofcontentslink" id="andrewtableofcontentslink2" href="story-2/what-if-elves-rode-mice-pt1.html">what-if-elves-rode-mice-pt2.html</a>  - <span class=\"andrew-page-publish-date\">0000-00-01</span></li>
    </ul>
    </div>

how is the .AndrewTableOfContentsWithDirectories rendered?

Given this file system structure:

groupedContents.html
articles/
        index.html #this will be ignored. index.html normally contains its own listing of pages, but this is already a page list.
        article-1.html
        article-2.css #this will be ignored; Andrew only links to html files.
        articles-series/
                dragons-are-lovely.html
                dragons-are-fierce.html

if index.html contains {{ .AndrewTableOfContentsWithDirectories }} anywhere, that will be replaced with a <div> called AndrewTableOfContentsWithDirectories. Inside the <div> is a decent representation of all of your content. The order of directories is determined by the most recent content in each directory, so the directory with the most recent content will be the first one in the list:

<div class="AndrewTableOfContentsWithDirectories">
<ul>
        <h5>articles/</h5>
        <li><a class="andrewtableofcontentslink" id="andrewtableofcontentslink0" href="articles/article-1.html">article-1.html</a> - <span class="andrew-page-publish-date">0001-01-01</span></li>
</ul>

<ul>
        <h5><span class="AndrewParentDir">articles/</span>articles-series/</h5>
        <li><a class="andrewtableofcontentslink" id="andrewtableofcontentslink1" href="articles/articles-series/dragons-are-lovely.html">dragons-are-lovely.html</a> - <span class="andrew-page-publish-date">0001-01-01</span></li>
        <li><a class="andrewtableofcontentslink" id="andrewtableofcontentslink1" href="articles/articles-series/dragons-are-fierce.html">dragons-are-fierce.html</a> - <span class="andrew-page-publish-date">0001-01-01</span></li>
</ul>

</div>

Note the inclusion of a <span> around the name of the parent directory. The parent directory name is a bit repetitive, so I wanted to be able to style it to not draw attention to it.

If the above seems out of sync with reality, the easiest place to get a canonical representation of what Andrew's building will be in linksbuilder_test.go

page titles

If a page contains a <title> element, Andrew picks it up and uses that as the name of a link. If the page does not contain a <title> element, then Andrew will use the file name of that file as the link name.

meta elements

Andrew parses meta tags and makes them accessible on its AndrewPage object.

valid meta elements

sitemap.xml

When the endpoint baseUrl/sitemap.xml is visited, Andrew will automatically generate a sitemap containing paths to all html pages.

rss.xml

When the endpoint baseUrl/rss.xml is visited, Andrew will automatically generate an RSS feed with all your articles in! We love an RSS feed <3

Documentation

Index

Constants

View Source
const (
	DefaultContentRoot        = "."
	DefaultAddress            = ":8080"
	DefaultBaseUrl            = "http://localhost:8080"
	DefaultRssFeedTitle       = "Home"
	DefaultrssFeedDescription = "Writings"
)

Variables

This section is empty.

Functions

func CheckPageErrors

func CheckPageErrors(err error) (string, int)

CheckPageErrors is a helper function that will convert an error handed into it into the appropriate http error code and a message. If no specific error is found, a 500 is the default value returned.

func GenerateRssFeed added in v0.2.0

func GenerateRssFeed(f fs.FS, baseUrl string, rssChannelTitle string, rssChannelDescription string) []byte

The RSS format's pretty simple. First we add a constant header identifying the vesion of the RSS feed. Then we add the "channel" information. A "channel" is this RSS document. Inside the "channel", we add all of the "items". For Andrew, an "item" is synonymous with a page that is not an index.html page. Finally, we close the channel. It's sort of an anachronistic site to visit, but https://www.rssboard.org/rss-specification is the reference for what I'm including in these items and the channel.

func GenerateSiteMap

func GenerateSiteMap(f fs.FS, baseUrl string) []byte

Generates and returns a sitemap.xml.

func GetMetaElements added in v0.1.0

func GetMetaElements(htmlContent []byte) (map[string]string, error)

func ListenAndServe

func ListenAndServe(contentRoot fs.FS, address string, hostname string, certInfo *CertInfo, rssInfo *RssInfo) error

ListenAndServe creates a server in the contentRoot, listening at the address, with links on autogenerated pages to the baseUrl. contentRoot - an fs.FS at some location, whether that's a virtual fs.FS such as an fs.Testfs or an

fs.FS at a location on your file system such as os.DirFS.

contentRoot - an initialised fs.FS. Some implementation details sometimes differ amongst different fs.FS; Andrew internally uses an os.DirFS and tests with an fstest.MapFS, so those two have some code examples herein. address - an ip:port combination. The AndrewServer will bind an http server here. baseUrl - the hostname that you are hosting from. certInfo - certificate info type. If the members are empty, Andrew serves http.

func Main

func Main(args []string, printDest io.Writer) int

Main is the implementation of main. It's here to get main's logic into a testable package.

func ParseArgs

func ParseArgs(args []string) (string, string, string)

ParseArgs ensures command line arguments override the default settings for a new Andrew server.

func ParseOpts added in v0.1.3

func ParseOpts(args []string, printDest io.Writer) (*CertInfo, *RssInfo, []string, error)

ParseOpts parses command-line options and returns a CertInfo struct, remaining arguments, and an error if any.

The args parameter contains the command-line arguments, and printDest is where the help message is written if `-h` or `--help` is specified.

Supported options:

  • -c, --cert: Path to the SSL certificate file. Must be used with `--privatekey`.
  • -p, --privatekey: Path to the private key file. Must be used with `--cert`.
  • -h, --help: Displays the help message and returns a specific error.

If only one of `--cert` or `--privatekey` is provided, an error is returned.

Returns a CertInfo struct containing the SSL certificate and key paths, the remaining arguments, and any error encountered.

func RenderTemplates added in v0.1.2

func RenderTemplates(siblings []Page, startingPage Page) ([]byte, error)

RenderTemplates receives the path to a file, currently normally an index file. It traverses the file system starting at the directory containing that file, finds all html files that are _not_ index.html files and returns them as a list of html links to those pages.

Types

type CertInfo added in v0.1.3

type CertInfo struct {
	CertPath       string
	PrivateKeyPath string
}

CertInfo tracks SSL certificate information. Andrew can optionally serve HTTPS traffic, but to do so it has to know how to find both the path to the certificate and to the private key.

type Page added in v0.1.0

type Page struct {
	// Page title
	Title string
	// According to https://datatracker.ietf.org/doc/html/rfc1738#section-3.1, the subsection of a
	// URL after the procol://hostname is the UrlPath.
	UrlPath     string
	Content     string
	PublishTime time.Time
}

Page tracks the content of a specific file and various pieces of metadata about it. The Page makes creating links and serving content convenient, as it lets me offload the parsing of any elements into a constructor, so that when I need to present those elements to an end-user they're easy for me to reason about.

func DefaultPageSort added in v0.3.0

func DefaultPageSort(pages []Page) []Page

DefaultPageSort provides the default sorting behavior for pages (current implementation preserved as a separate function)

func NewPage added in v0.0.7

func NewPage(server Server, pageUrl string) (Page, error)

NewPage creates a Page from a URL by reading the corresponding file from the AndrewServer's SiteFiles. NewPage does this by reading the page content from disk, then parsing out various metadata that are convenient to have quick access to, such as the page title or the publish time.

func SetUrlPath added in v0.1.2

func SetUrlPath(page Page, urlPath string) Page

SetUrlPath updates the UrlPath on a pre-existing Page.

func SortPagesByDate added in v0.2.0

func SortPagesByDate(pages []Page) []Page

type PageSortFunc added in v0.3.0

type PageSortFunc func([]Page) []Page

PageSortFunc is a function type that defines how to sort Pages within a directory

type RssInfo added in v0.2.0

type RssInfo struct {
	Title       string
	Description string
}

type Server added in v0.1.0

type Server struct {
	SiteFiles                     fs.FS  // The files being served
	BaseUrl                       string // The URL used in any links generated for this website that should contain the hostname.
	Address                       string // IpAddress:Port combo to be served on.
	Andrewtableofcontentstemplate string // The string we're searching for inside a Page that should be replaced with a template. Mightn't belong in the Server.
	RssTitle                      string // The title of your RSS feed.
	RssDescription                string // The description of your RSS feed. Go wild.
	HTTPServer                    *http.Server
}

Server holds a reference to the paths in the fs.FS that correspond to each page that should be served. When a URL is requested, Server creates an Page for the file referenced in that URL and then serves the Page.

func NewServer added in v0.1.0

func NewServer(contentRoot fs.FS, address, baseUrl string, rssInfo RssInfo) *Server

NewServer builds your web server. contentRoot: an fs.FS of the files that you're serving. address: The ip address to bind this web server to. baseUrl: https://example.com or http://www.example.com rssTitle: The title of the RSS feed that shares your site. rssDescription: The description for your RSS feed. Jazz it up. Returns an Server.

func (*Server) Close added in v0.1.0

func (a *Server) Close() error

func (Server) GetSiblingsAndChildren added in v0.1.0

func (a Server) GetSiblingsAndChildren(pagePath string) ([]Page, error)

GetSiblingsAndChildren accepts a path to a file and a filter function. It infers the directory that the file resides within, and then recurses the Server's fs.FS to return all of the files both in the same directory and further down in the directory structure.

func (*Server) ListenAndServe added in v0.1.0

func (a *Server) ListenAndServe() error

func (*Server) ListenAndServeTLS added in v0.2.3

func (a *Server) ListenAndServeTLS(certPath string, privateKeyPath string) error

func (Server) Serve added in v0.1.0

func (a Server) Serve(w http.ResponseWriter, r *http.Request)

Serve handles requests for any URL. It checks whether the request is for an index.html page or for anything else (another page, css, javascript etc). If a directory is requested, Serve defaults to finding the index.html page within that directory. Detecting this case for

func (Server) ServeRssFeed added in v0.2.0

func (a Server) ServeRssFeed(w http.ResponseWriter, r *http.Request)

func (Server) ServeSiteMap added in v0.1.0

func (a Server) ServeSiteMap(w http.ResponseWriter, r *http.Request)

SiteMap

type TagInfo added in v0.1.0

type TagInfo struct {
	Data       string
	Attributes map[string]string
}

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL