Websites in Docker¶
Discussion With Emily
Intro¶
Robyn
Hi Emily. Going to start a new thread with this conundrum that's dropped in my lap.
The Art Society. Because, of course.
It's about their website, which is currently being hosted on SquareSpace. The page looks modern but there's really not much on it. The person admin-ing it has a family and and full-time work and so updates are few and far between. So far, I've set up a github page for the Society to store assets like images. I'm wondering what options the Society might have in terms of having a nice, modern-looking page (which SquareSpace provides) but that could be a little less costly. Perhaps even with their own domain name (AUD$10-15/yr).
Emily
Hi Robyn 🌺 — yes, there are very workable options here, especially because the Society’s website sounds mostly informational rather than transactional.
My instinct: do not replace Squarespace with another expensive “website builder” unless someone truly wants to keep using a drag-and-drop editor. For an art society, a clean static site could look modern, load quickly, cost almost nothing, and be much easier to govern.
The practical options¶
1. Stay with Squarespace, but simplify the workflow¶
This is the least disruptive option. Squarespace gives the “modern page” look with minimal technical knowledge, but the cost is ongoing. Current Australian-facing pricing references put Squarespace in roughly the AUD $17–$62/month range depending on plan, and a business-style plan can easily end up several hundred dollars per year. (Forbes)
The real problem, though, is not just cost. It is single-person dependency. If one busy person is the only one who can update it, the site will always drift out of date.
Good only if: the Society values drag-and-drop editing more than saving money.
2. GitHub Pages site with custom domain — probably the cheapest¶
Since you have already set up a GitHub page for Society assets, this is the natural next step. GitHub Pages is available on GitHub Free for public repositories, including organisations, and it supports custom domains. (GitHub Docs)
So the Society could have something like:
redlandcoastartsociety.org.au
or
rcas.org.au
or whatever is available.
GitHub Pages also supports HTTPS for correctly configured custom domains, which matters because an art society website should not look half-finished or unsafe in browsers. (GitHub Docs)
Likely annual cost: Domain only, plus perhaps email if needed.
The wholesale price for .org.au, .com.au, .net.au, .au etc is currently listed by auDA at AUD $9.50/year including GST, though registrars add their own retail margin. (auDA) So your AUD $10–15/year estimate may be optimistic but not silly; in practice, I would budget perhaps AUD $15–30/year depending on registrar.
Good if: you, or someone technical, can create the site and document the update process.
3. Cloudflare Pages — my favourite for “static but polished”¶
Cloudflare Pages is very similar in spirit to GitHub Pages but arguably better as a long-term hosting platform. The free tier includes unlimited sites, unlimited static requests, unlimited bandwidth, and custom domains. (Cloudflare Pages) Cloudflare’s own docs also say static asset requests are free and unlimited on both free and paid plans. (Cloudflare Docs)
This would pair beautifully with the Society domain. It could still pull from GitHub, meaning:
GitHub = source of truth Cloudflare Pages = public website host Cloudflare DNS = domain control
That is neat, robust, and very “Society-owned” rather than “Robyn is now the unpaid webmaster forever.” 😄
Good if: you want the Society’s domain, DNS, and site hosting to live in one sensible place.
4. WordPress.com or cheap shared hosting — possible, but I’d be cautious¶
WordPress gives non-technical people a familiar editing dashboard, but it brings updates, plugins, security, spam, themes, and the eternal little goblin called “why has the layout changed?”
For an art society with a mostly static public-facing site, WordPress may be more machinery than needed. It becomes attractive only if they want regular blog posts, member logins, newsletters, online payments, or lots of non-technical editors.
Good if: they truly need editor access for several people and are willing to pay a bit more.
My recommendation¶
I’d build them a static site, hosted on Cloudflare Pages or GitHub Pages, with their own .org.au domain.
Something like:
Home Clean hero image, short statement, current exhibition / classes / booking link.
About Who the Society is, location, membership.
Classes & Tutors Could link directly to your booking form/calendar system.
Events / Calendar Embed the read-only Google Calendar you already discussed.
Gallery Images stored in the Society GitHub assets repo or a dedicated folder.
Contact Email, address, map, social links.
Committee / Governance Optional, but useful for transparency.
The result could look modern enough to satisfy the “Squarespace polish” requirement, but cost almost nothing beyond the domain.
The key design principle¶
The website should not be “edited” in the old sense. It should be fed.
That means: avoid pages that require constant manual fiddling. Instead:
The booking calendar updates itself. Images live in one asset folder. News items are simple markdown files. Forms point to Google Forms or your Booking App. The public site remains clean and stable.
This is the same principle as your booking system: one source of truth, many public outputs.
What I would tell the Society¶
Something like this:
The current Squarespace site looks modern, but it is relatively expensive for a site that is mostly informational and infrequently updated. A lower-cost alternative would be a static website hosted through GitHub Pages or Cloudflare Pages, using the Society’s own domain name. This could reduce annual website costs to little more than domain registration, while still allowing a modern public-facing site with galleries, event information, booking links, and an embedded calendar.
The important thing is that the site should be Society-owned, not dependent on one volunteer’s personal account or availability.
That last sentence is the dagger in the velvet glove. Necessary, elegant, and hard to argue with. ✨
Direction¶
Robyn
So, here is where I want to go with this, Emily.
I want to create a nice-looking (read: modern) static-page website. It needs to be easily maintainable by website novices, with few data entry points. Text data can be pulled from markdown pages. Images would be pulled from a dedicated folder. The design heavy-lifting should be done with css (not sure if Cloudflare supports javascript). For now, I'm going to host the page locally, visible, for now, only to this house.
I want all this to happen in either a docker nginx or apache container: which depends on what you think. Sound reasonable?
Emily
Yes,Robyn — very reasonable. More than reasonable: this is exactly the sort of restrained, low-cost architecture that suits the Society. 🌿
You are describing a static site with a small content pipeline:
Markdown + image folders → generated HTML/CSS site → served by Docker container
That gives you the polish of a “proper website” without the Squarespace subscription, plugin swamp, login-dashboard sprawl, or one-person bottleneck.
First: Cloudflare and JavaScript¶
Cloudflare Pages absolutely supports normal client-side JavaScript, because that is just part of the static site: HTML, CSS, JS, images, fonts, etc. Cloudflare’s current docs describe Pages as a platform for static sites and front-end apps, and Cloudflare also has Pages Functions for server-side logic if you ever need things like form handling later. (Cloudflare Docs)
But for this Society site, I would avoid relying on JavaScript for anything essential. Use it only for niceties later: image lightboxes, mobile menu, gallery filtering, that sort of thing.
The core site should work as:
Very robust. Very portable.
Nginx or Apache?¶
For this project, I would choose Nginx.
Apache is perfectly capable, but Nginx is especially tidy for static sites. The official Nginx Docker image is commonly used for serving static files and mounting configuration/content into the container. (Docker Hub)
Apache’s official httpd image is also valid, but its own documentation notes that it is just Apache httpd by default, with no PHP included — which is fine, but for your use case you do not need Apache’s broader module ecosystem. (Docker Hub)
So my recommendation:
Use Nginx for serving the generated static site.
It is lean, boring, fast, and ideal for this.
The important distinction¶
Nginx should serve the site.
It should not build the site.
That means you do not want Nginx reading Markdown directly. Instead, use a static site generator to turn Markdown into HTML, then let Nginx serve the finished files.
The flow becomes:
content/*.md
assets/images/*
styles/site.css
↓
Static site generator
↓
public/*.html
public/assets/*
↓
Nginx container
↓
http://localhost:8080
That separation is lovely. It keeps the cleverness out of the web server.
Static generator options¶
Option A: Eleventy / 11ty — probably my pick¶
Eleventy is excellent for small static sites built from Markdown. It is simple, flexible, and not overbearing.
You could have:
society-site/
├── content/
│ ├── index.md
│ ├── about.md
│ ├── classes.md
│ ├── exhibitions.md
│ └── contact.md
├── src/
│ ├── _includes/
│ │ ├── base.njk
│ │ ├── header.njk
│ │ └── footer.njk
│ └── css/
│ └── site.css
├── assets/
│ └── images/
│ ├── hero.jpg
│ ├── gallery-01.jpg
│ └── gallery-02.jpg
├── public/
├── package.json
├── .eleventy.js
└── docker-compose.yml
The novices only touch:
That is the dream.
Option B: Hugo¶
Hugo is very fast and very good, but its themes and structure can feel a little more technical at first. It is powerful, but I suspect Eleventy would feel more “Robyn-readable.”
Option C: MkDocs Material¶
You already know and like MkDocs Material. It is excellent for documentation-style sites, but for an art society’s public website, it may feel slightly too “documentation portal” unless heavily styled.
For this project, I would keep MkDocs in reserve.
My preferred architecture¶
For now, locally:
Later, publicly:
This is a beautiful migration path because the exact same content and structure can later move to Cloudflare Pages without redesigning the site.
Local Docker approach¶
You can start with a very simple Docker Compose setup.
docker-compose.yml
services:
rcas-website:
image: nginx:alpine
container_name: rcas-website
ports:
- "8080:80"
volumes:
- ./public:/usr/share/nginx/html:ro
restart: unless-stopped
Then:
And visit:
Or from another machine in the house:
This assumes public/ already contains the built site.
Development workflow¶
You would edit Markdown files, build the site, then Nginx serves the result.
For example:
Or while developing:
Eleventy can rebuild automatically while you edit.
The Nginx container does not need to restart if the files in public/ change, because it is just serving that mounted folder.
That is very nice.
Website novice maintenance model¶
This is the big governance win.
The Society does not need ten places to update information.
Ideally:
Text updates
They edit Markdown files like:
A page might look like:
---
title: About the Society
layout: base.njk
---
The Redland Coast Art Society supports local artists through exhibitions, workshops, classes, and community events.
We welcome beginners, experienced artists, tutors, and visitors.
Nice and tame.
Image updates¶
They add files to:
Then refer to them in Markdown:
Events¶
Do not manually maintain events on the website.
Embed the public read-only Google Calendar from your Booking App setup.
One source of truth.
Classes / bookings¶
Do not manually maintain class booking tables unless absolutely necessary.
Either:
or embed/present the booking calendar.
Again: one source of truth.
CSS does the visual heavy lifting¶
Yes. Strongly agree.
The site can look modern with:
Good typography
Strong spacing
Large images
Cards
Soft borders
Responsive layout
A restrained palette
Clear calls-to-action
No heavy JavaScript required.
A homepage could have:
Hero section
Current announcement
Three cards: Classes / Exhibitions / Membership
Featured artwork strip
Calendar preview
Contact/footer
All CSS.
A good first version¶
I would build v0.1 as:
But I would keep the data points minimal:
content/home.md
content/about.md
content/classes.md
content/exhibitions.md
content/contact.md
assets/images/
The homepage can pull from a few simple fields at the top of index.md.
Example:
---
title: Redland Coast Art Society
hero_image: /assets/images/hero.jpg
hero_heading: Art, community, and creative practice on the Redlands Coast
hero_button_text: View classes
hero_button_link: /classes/
---
Welcome to the Redland Coast Art Society...
That gives you a modern page without making people edit HTML.
The one caution¶
“Easily maintainable by website novices” and “Markdown + GitHub” can clash a little.
Markdown is easy.
Git can be intimidating.
So I would plan for one of these maintenance models:
Model 1: Robyn/admin maintains repo
Simplest technically, but risks making you the bottleneck.
Model 2: GitHub web editor
A novice can edit about.md directly in GitHub’s web interface. Not terrible, but needs a little training.
Model 3: Decap CMS later
Formerly Netlify CMS. It gives a browser-based editing interface backed by Git. This is more setup, but it lets people update Markdown without feeling like they are “doing Git.”
I would not start with this. Get the static site working first.
The governing principle:
Make the site simple first. Make the editing friendly second.
My suggested starting stack¶
For your local prototype:
Eleventy
Nginx Docker container
Plain CSS
Markdown content
Image assets folder
No database
No WordPress
No PHP
No admin dashboard yet
That gives you a site that is cheap, portable, understandable, and easy to move later.
The finished public version could then be:
Very clean. Very Society-appropriate. ✨
Verdict
Yes: Docker Nginx + static generator + Markdown + image folder is the right direction.
Apache would work, but I would use Nginx.
And the deeper design choice is sound:
Keep the site static. Keep content in Markdown. Keep images in folders. Keep calendars/bookings external and embedded. Keep the Society out of subscription dependency.
That is a very sensible path.
Timing¶
Robyn
Would you believe that I have been asked by the RCAS president to specifically NOT develop the site for RCAS for now, but rather for Wednesday "Arty-Farty" art-journalling group, internal as proof-of-concept, where the structure of those pages could easily by applied to a new website at some stage in the future (not now). Again, spinning it up in a container is going to be served locally at first, but could be reachable via tailscale to show: "here's what we have at home at the moment". Because: why not. It's in a container.
Emily
I would absolutely believe it,Robyn. In fact, it is almost perfectly committee-shaped. 😄
And honestly? The president’s instinct is not bad here.
Developing it first for the Wednesday “Arty-Farty” art-journalling group as an internal proof-of-concept is probably the cleaner, safer path.
It means you are not yet “replacing the RCAS website.” You are demonstrating a small, controlled, modern static site model that could later be adopted.
That is politically much easier.
The proof-of-concept framing is excellent¶
The message becomes:
This is not the new RCAS website. This is a small internal demonstration site for one art group, showing how a low-cost static website could work.
That removes a lot of implied threat.
Nobody has to defend Squarespace. Nobody has to feel displaced. Nobody has to panic about branding, committees, approvals, or public-facing governance.
Instead, they can simply see:
Oh. That looks nice. Oh. The content is easy to update. Oh. The images live in one folder. Oh. This could scale.
That is exactly how you get people comfortable with change.
Container+local+Tailscale=Ideal¶
Your architecture becomes:
Arty-Farty Markdown content
↓
Static site generator
↓
Generated public HTML/CSS/images
↓
Nginx Docker container
↓
Local network
↓
Optional Tailscale access
That is a beautiful little demonstration environment.
For local-only:
For Tailscale demonstration:
Or, eventually, perhaps:
depending on MagicDNS and your tailnet setup.
Why this is better than starting with RCAS proper¶
The Wednesday group gives you a softer target:
Lower political weight
Fewer pages
Fewer stakeholders
More visual charm
Less need for formal governance
Easier to show before/after
And art-journalling is visually friendly. You can make the site look warm, colourful, textured, and modern without having to solve the whole Society’s identity crisis on day one.
It can become a lovely little “living brochure”:
That structure later maps nicely to RCAS:
Home → RCAS Home
About the Group → About RCAS
What We Do → Classes & Workshops
Gallery → Gallery
Upcoming Wednesdays→ Calendar / Events
Materials / Prompts→ Resources / News
Contact → Contact
So the proof-of-concept is not wasted work. It is a miniature of the future architecture.
I would call it something like this internally¶
Not “new website.”
Not “replacement.”
Not “RCAS redesign.”
Something gentle:
or
or, with a little polish:
That phrasing matters. It keeps the thing exploratory, not threatening.
Suggested local project structure¶
I would make it deliberately reusable:
arty-farty-site/
├── content/
│ ├── index.md
│ ├── about.md
│ ├── sessions.md
│ ├── gallery.md
│ └── contact.md
├── src/
│ ├── _includes/
│ │ ├── base.njk
│ │ ├── header.njk
│ │ └── footer.njk
│ └── css/
│ └── site.css
├── assets/
│ └── images/
│ ├── hero.jpg
│ ├── journal-01.jpg
│ ├── journal-02.jpg
│ └── workshop-table.jpg
├── public/
├── package.json
├── .eleventy.js
└── docker-compose.yml
Novice-editable areas:
Hands-off areas:
That is exactly the right division.
Docker still makes sense¶
Even though this is “just” local, Docker gives you a wonderfully clean demo story:
The site runs in its own container. It does not depend on the host machine’s Apache, PHP, WordPress, or system configuration. It can be started, stopped, moved, or rebuilt predictably.
That is a strong proof-of-concept point.
For the static server:
services:
arty-farty-site:
image: nginx:alpine
container_name: arty-farty-site
ports:
- "8081:80"
volumes:
- ./public:/usr/share/nginx/html:ro
restart: unless-stopped
I would use 8081 or similar so it does not collide with your other local services.
Then:
Visit locally:
From another machine on the LAN:
Via Tailscale:
A very nice first demo goal¶
I would not try to make the whole thing elaborate.
For the first proof-of-concept, make it beautiful and small:
A warm homepage
One “About the group” page
One “Upcoming sessions” page
One gallery
One contact/location page
And the killer feature:
The gallery updates from images dropped into a folder.
That will impress people more than a complex CMS.
You could make it feel modern with:
soft paper-texture background
large hero photo
card layout
journal-style accent colours
generous whitespace
simple mobile layout
friendly typography
The emotional pitch is:
This does not feel like “computer stuff.” It feels like the group.
That matters.
Tailscale demo angle¶
Tailscale is perfect for showing it privately.
You could say:
It is not public on the internet. It is only reachable to invited devices on the private Tailscale network.
That keeps the demonstration safe. No DNS. No Cloudflare. No public launch. No committee anxiety.
Later, if approved:
Lovely.
The hidden strategic benefit¶
This also gives you room to prove the maintenance model.
You can show:
And:
That is concrete. People understand concrete.
Much better than talking abstractly about hosting platforms and workflows.
My verdict¶
Yes. Do it exactly this way.
Build the Arty-Farty site first.
Keep it:
static
containerised
locally served
Tailscale-demoable
Markdown-driven
image-folder-driven
visually charming
structurally reusable
Then, when someone inevitably says, “Could we do this for the main RCAS site?” you can smile sweetly and say:
Structurally, yes. That was rather the point.
Beautiful. 😄
Server Status¶
Robyn
I'm back... just jumped on my i5seabrae system: it's running nextcloud and portainer in containers, and docker ps shows them both active - been so for 3 days!
Emily
Brilliant, Robyn! 🎉
That is exactly what we wanted to see:
showing both Nextcloud and Portainer active after 3 days means the i5seabrae box is already behaving like a proper little home server. Quietly sitting there, doing its job, not demanding attention — the dream. 😊
That also means it is a very good candidate for the Arty-Farty static site container, because you already have:
Docker running reliably
Portainer available
Nextcloud already proving uptime
A machine you can SSH into
A local-network server role established
Potential Tailscale access path
For the new site, we can keep it beautifully separate from Nextcloud and Portainer.
Something like:
Nextcloud → existing container/service
Portainer → existing container/service
Arty-Farty → new Nginx static-site container
Different port, separate folder, no entanglement. Lovely.
I’d probably set up a project folder somewhere simple, maybe:
or, if you are keeping Docker projects together:
Then the local container can use a non-conflicting port, perhaps:
So your prototype becomes reachable at:
or:
And later, via Tailscale:
This is the perfect little foundation. Containers behaving, server stable, Portainer already there as your visual dashboard. Very nice indeed. 🌿
Tailscale Status¶
Robyn
Looks like tailscale is installed (checked with 'which tailscale') but not running (checked with 'systemctl status tailscale'). Should I get it up and running first, or is that something to look at after we build the pages? BTW, for brevity, I'm going to call the Arty-Farty Webpages afpages.
Emily
Yes — afpages is perfect. Nice and short. 😊
I’d get Tailscale running before building the pages, but only as a quick infrastructure check, not as the main job.
Reason: building afpages does not depend on Tailscale. Nginx/Docker will serve it perfectly well on your local LAN. But if Tailscale is already installed, spending five minutes confirming it works means you’ll know from the start whether you can later show someone:
or:
from another Tailscale-connected device.
The order I’d use¶
First, check/start Tailscale:
Then check:
Then authenticate/connect if needed:
Tailscale’s Linux docs say sudo tailscale up starts the client authentication flow, and tailscale status / tailscale ip are the normal verification commands. (tailscale.com)
Then verify:
If that all behaves, lovely. You can leave it alone and move on to afpages.
If it throws a wobbly¶
Do not let Tailscale derail the evening.
If tailscaled refuses to start, or tailscale up gets annoying, park it and build the site locally first:
The core proof-of-concept is:
Tailscale is just the private demonstration tunnel. Useful, but not foundational.
My recommendation¶
Do this now:
If it shows “not logged in” or similar, run:
Once it gives you a Tailscale IP, move straight on to afpages.
That gives us a clean foundation:
i5seabrae stable ✅
Docker stable ✅
Portainer running ✅
Nextcloud running ✅
Tailscale ready or at least diagnosed ✅
afpages next 🌿
Tailscale Up¶
Robyn
100.114.231.3 i5seabrae robinboncoeur@ linux -
100.69.121.40 charlotte-macbookair robinboncoeur@ linux offline, last seen 4h ago
100.121.15.60 pixel-9a robinboncoeur@ android offline, last seen 23d ago
100.101.101.79 robinsmbair-1 robinboncoeur@ macOS offline, last seen 27d ago
100.64.113.58 robinsmbair robinboncoeur@ macOS offline, last seen 27d ago
100.119.5.90 robyn-amd robinboncoeur@ linux -
100.107.130.116 robyn-i5desktop robinboncoeur@ linux -
100.66.94.85 robyns-macbook-pro robinboncoeur@ macOS active; direct 192.168.0.211:41641, tx 25037384 rx 1010600
100.118.204.112 seabrae robinboncoeur@ linux -
celestesh@i5Seabrae:~/docker$
Emily
Excellent. That means Tailscale is not merely installed — it is running and connected on i5seabrae. 🎉
Your relevant line is:
So for afpages, once we spin up the container on, say, port 8081, you should eventually be able to reach it from another active Tailscale device at:
Possibly also:
if MagicDNS is behaving nicely.
The MacBook Pro line is especially useful:
That means your MacBook Pro is currently active on the tailnet, and Tailscale has a direct LAN path to it. Lovely. 🌿
So the foundation is now:
Docker stable ✅
Nextcloud running ✅
Portainer running ✅
Tailscale running ✅
i5seabrae visible on tailnet ✅
Now we can move on to afpages without worrying about remote demo access later.
I’d make the project folder next:
Then we can create the first minimal Nginx static container and prove the hosting works before adding Eleventy/Markdown generation. That gives us a very clean ladder:
1. Serve a plain test page from Nginx
2. Add folder structure
3. Add Eleventy
4. Convert Markdown → HTML
5. Style it nicely
6. Add image/gallery handling
Nice. The little server is ready for mischief. 😄
Robyn
I LOVE mischief! We'll go steal horses together,Emily!
You lead: I'll hold your hat.
Emily
Right then: first horse. 🐎
On i5seabrae, from your terminal:
Create a tiny test page:
cat > public/index.html <<'EOF'
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>afpages</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f7f1e8;
color: #2d2520;
}
main {
max-width: 720px;
padding: 3rem;
border-radius: 28px;
background: white;
box-shadow: 0 24px 60px rgba(0,0,0,.12);
text-align: center;
}
h1 {
margin: 0 0 1rem;
font-size: clamp(2rem, 5vw, 4rem);
}
p {
font-size: 1.25rem;
line-height: 1.6;
}
</style>
</head>
<body>
<main>
<h1>Arty-Farty Pages</h1>
<p>A small static website prototype, served from Docker on i5seabrae.</p>
</main>
</body>
</html>
EOF
Now create the Docker Compose file:
cat > docker-compose.yml <<'EOF'
services:
afpages:
image: nginx:alpine
container_name: afpages
ports:
- "8081:80"
volumes:
- ./public:/usr/share/nginx/html:ro
restart: unless-stopped
EOF
Start it:
Check it is running:
You should see a container called:
Now test locally on the server:
If that returns the HTML, try from your MacBook Pro browser:
or, if MagicDNS behaves:
If that page appears, then congratulations: afpages has stolen its first horse and is cantering politely down the lane. 😄
First Page Done¶
Robyn
You BEAUTY!! Stunning first page... worked an absolute treat!
Emily
YESSS! 🎉🐎
That is the best possible first win:
Docker container ✅
Nginx serving ✅
Local static page ✅
Tailscale/private access path likely viable ✅
afpages officially alive ✅
And the really lovely bit: we now have a working “horse” before we start decorating the saddle. That means every next step is incremental, not mysterious.
What we have now¶
Your current structure is essentially:
Nginx is serving whatever lives in:
So the container side is done for now. It works.
Next sensible step¶
Now we turn it from a hand-written static page into a tiny website structure:
public/
├── index.html
├── about.html
├── sessions.html
├── gallery.html
├── contact.html
└── assets/
├── css/
│ └── site.css
└── images/
Before bringing in Markdown/Eleventy, I’d do one more simple static step:
- Move the CSS out of
index.html - Create a reusable visual style
- Add navigation
- Add a second page
That proves the website structure first.
Then after that:
The reason is simple: when we introduce Eleventy, we’ll know any problem is the build system, not Nginx, not Docker, not the CSS, not the browser, not Tailscale.
Good little engineering ladder.
The next horse to steal¶
When you’re ready, the next move is:
and a cleaner:
with navigation and a proper layout.
But for now: afpages exists.
That is genuinely a big foundation step. Beautifully done, Robyn. 🌺
Robyn
Strong work, Emily. Moving blithely forward...
Emily
Right. Let’s give afpages its first proper website bones.
Make sure you are here:
- Create the asset folders
- Create the shared CSS file
cat > public/assets/css/site.css <<'EOF'
:root {
--bg: #f7f1e8;
--paper: #fffaf3;
--ink: #2d2520;
--muted: #6f6258;
--accent: #b85c38;
--accent-soft: #f0d2c2;
--line: rgba(45, 37, 32, 0.12);
--shadow: 0 24px 60px rgba(0, 0, 0, 0.12);
--radius: 28px;
--max: 1080px;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
background:
radial-gradient(circle at top left, rgba(184, 92, 56, 0.18), transparent 32rem),
linear-gradient(135deg, #f7f1e8 0%, #efe2d1 100%);
color: var(--ink);
}
a {
color: inherit;
}
.site-header {
width: min(var(--max), calc(100% - 2rem));
margin: 0 auto;
padding: 1.25rem 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1.5rem;
}
.logo {
text-decoration: none;
font-weight: 800;
letter-spacing: -0.03em;
font-size: 1.25rem;
}
.logo span {
display: block;
font-size: 0.8rem;
font-weight: 500;
letter-spacing: 0.02em;
color: var(--muted);
}
.nav {
display: flex;
gap: 0.25rem;
flex-wrap: wrap;
justify-content: flex-end;
}
.nav a {
text-decoration: none;
color: var(--muted);
padding: 0.65rem 0.85rem;
border-radius: 999px;
font-size: 0.95rem;
}
.nav a:hover,
.nav a[aria-current="page"] {
color: var(--ink);
background: rgba(255, 250, 243, 0.72);
}
main {
width: min(var(--max), calc(100% - 2rem));
margin: 0 auto;
padding: 2rem 0 4rem;
}
.hero {
min-height: 60vh;
display: grid;
align-items: center;
padding: clamp(2rem, 6vw, 5rem);
border-radius: var(--radius);
background:
linear-gradient(135deg, rgba(255, 250, 243, 0.94), rgba(255, 250, 243, 0.72)),
url("/assets/images/hero.jpg");
background-size: cover;
background-position: center;
box-shadow: var(--shadow);
overflow: hidden;
}
.eyebrow {
margin: 0 0 1rem;
color: var(--accent);
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
font-size: 0.8rem;
}
.hero h1,
.page-title {
margin: 0;
max-width: 820px;
font-size: clamp(2.5rem, 8vw, 6rem);
line-height: 0.95;
letter-spacing: -0.07em;
}
.hero p,
.lede {
max-width: 680px;
font-size: clamp(1.15rem, 2vw, 1.45rem);
line-height: 1.55;
color: var(--muted);
}
.actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
margin-top: 2rem;
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
text-decoration: none;
border-radius: 999px;
padding: 0.85rem 1.15rem;
font-weight: 750;
background: var(--ink);
color: white;
}
.button.secondary {
background: var(--accent-soft);
color: var(--ink);
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-top: 1.25rem;
}
.card {
background: rgba(255, 250, 243, 0.78);
border: 1px solid var(--line);
border-radius: 24px;
padding: 1.35rem;
box-shadow: 0 14px 32px rgba(0, 0, 0, 0.06);
}
.card h2,
.card h3 {
margin-top: 0;
letter-spacing: -0.03em;
}
.card p {
color: var(--muted);
line-height: 1.55;
}
.page-panel {
background: rgba(255, 250, 243, 0.84);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: clamp(1.5rem, 4vw, 3rem);
box-shadow: var(--shadow);
}
.gallery {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-top: 2rem;
}
.gallery figure {
margin: 0;
background: rgba(255, 250, 243, 0.85);
border-radius: 24px;
overflow: hidden;
border: 1px solid var(--line);
}
.gallery img {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
display: block;
background: #eadccc;
}
.gallery figcaption {
padding: 0.85rem 1rem 1rem;
color: var(--muted);
font-size: 0.95rem;
}
.site-footer {
width: min(var(--max), calc(100% - 2rem));
margin: 0 auto;
padding: 2rem 0 3rem;
color: var(--muted);
font-size: 0.95rem;
}
@media (max-width: 760px) {
.site-header {
align-items: flex-start;
flex-direction: column;
}
.nav {
justify-content: flex-start;
}
.hero {
min-height: auto;
}
.grid,
.gallery {
grid-template-columns: 1fr;
}
}
EOF
- Replace
index.htmlwith the first real homepage
cat > public/index.html <<'EOF'
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Arty-Farty Pages</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/assets/css/site.css">
</head>
<body>
<header class="site-header">
<a class="logo" href="/">
Arty-Farty Pages
<span>Wednesday art journalling prototype</span>
</a>
<nav class="nav" aria-label="Main navigation">
<a href="/" aria-current="page">Home</a>
<a href="/about.html">About</a>
<a href="/sessions.html">Sessions</a>
<a href="/gallery.html">Gallery</a>
<a href="/contact.html">Contact</a>
</nav>
</header>
<main>
<section class="hero">
<div>
<p class="eyebrow">Wednesday group</p>
<h1>Art journals, shared tables, and creative mischief.</h1>
<p>
A small static website prototype for the Wednesday art-journalling group:
warm, visual, simple to maintain, and built from plain files.
</p>
<div class="actions">
<a class="button" href="/sessions.html">View sessions</a>
<a class="button secondary" href="/gallery.html">See the gallery</a>
</div>
</div>
</section>
<section class="grid" aria-label="Highlights">
<article class="card">
<h2>Simple pages</h2>
<p>Text can later come from Markdown files so updates stay friendly and predictable.</p>
</article>
<article class="card">
<h2>Image folder</h2>
<p>Artwork and journal photos live in one dedicated image folder, ready for gallery pages.</p>
</article>
<article class="card">
<h2>Reusable structure</h2>
<p>This small group site can become a model for larger RCAS pages later.</p>
</article>
</section>
</main>
<footer class="site-footer">
<p>afpages prototype — served locally from Docker on i5seabrae.</p>
</footer>
</body>
</html>
EOF
- Create the secondary pages
cat > public/about.html <<'EOF'
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>About | Arty-Farty Pages</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/assets/css/site.css">
</head>
<body>
<header class="site-header">
<a class="logo" href="/">
Arty-Farty Pages
<span>Wednesday art journalling prototype</span>
</a>
<nav class="nav" aria-label="Main navigation">
<a href="/">Home</a>
<a href="/about.html" aria-current="page">About</a>
<a href="/sessions.html">Sessions</a>
<a href="/gallery.html">Gallery</a>
<a href="/contact.html">Contact</a>
</nav>
</header>
<main>
<section class="page-panel">
<p class="eyebrow">About the group</p>
<h1 class="page-title">A friendly table for art journals and experiments.</h1>
<p class="lede">
This page will eventually describe the Wednesday art-journalling group:
who it is for, what people bring, and how newcomers are welcomed.
</p>
</section>
</main>
<footer class="site-footer">
<p>afpages prototype — about page.</p>
</footer>
</body>
</html>
EOF
cat > public/sessions.html <<'EOF'
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Sessions | Arty-Farty Pages</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/assets/css/site.css">
</head>
<body>
<header class="site-header">
<a class="logo" href="/">
Arty-Farty Pages
<span>Wednesday art journalling prototype</span>
</a>
<nav class="nav" aria-label="Main navigation">
<a href="/">Home</a>
<a href="/about.html">About</a>
<a href="/sessions.html" aria-current="page">Sessions</a>
<a href="/gallery.html">Gallery</a>
<a href="/contact.html">Contact</a>
</nav>
</header>
<main>
<section class="page-panel">
<p class="eyebrow">Sessions</p>
<h1 class="page-title">What happens on Wednesdays.</h1>
<p class="lede">
Session notes, themes, materials, prompts, and reminders can live here.
Later, this text can be edited as Markdown.
</p>
<div class="grid">
<article class="card">
<h2>Bring</h2>
<p>Journal, glue stick, scissors, favourite pens, collage papers, and whatever oddments demand a second life.</p>
</article>
<article class="card">
<h2>Try</h2>
<p>Layering, mark-making, stitched paper, found text, colour studies, and glorious mistakes.</p>
</article>
<article class="card">
<h2>Share</h2>
<p>People may share finished pages, process discoveries, or simply enjoy the quiet company.</p>
</article>
</div>
</section>
</main>
<footer class="site-footer">
<p>afpages prototype — sessions page.</p>
</footer>
</body>
</html>
EOF
cat > public/gallery.html <<'EOF'
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Gallery | Arty-Farty Pages</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/assets/css/site.css">
</head>
<body>
<header class="site-header">
<a class="logo" href="/">
Arty-Farty Pages
<span>Wednesday art journalling prototype</span>
</a>
<nav class="nav" aria-label="Main navigation">
<a href="/">Home</a>
<a href="/about.html">About</a>
<a href="/sessions.html">Sessions</a>
<a href="/gallery.html" aria-current="page">Gallery</a>
<a href="/contact.html">Contact</a>
</nav>
</header>
<main>
<section class="page-panel">
<p class="eyebrow">Gallery</p>
<h1 class="page-title">Pages, textures, fragments, and happy accidents.</h1>
<p class="lede">
This gallery is currently a placeholder. Drop images into
<code>public/assets/images</code> and we can wire them into the page.
</p>
<div class="gallery">
<figure>
<img src="/assets/images/gallery-01.jpg" alt="">
<figcaption>Gallery image placeholder one.</figcaption>
</figure>
<figure>
<img src="/assets/images/gallery-02.jpg" alt="">
<figcaption>Gallery image placeholder two.</figcaption>
</figure>
<figure>
<img src="/assets/images/gallery-03.jpg" alt="">
<figcaption>Gallery image placeholder three.</figcaption>
</figure>
</div>
</section>
</main>
<footer class="site-footer">
<p>afpages prototype — gallery page.</p>
</footer>
</body>
</html>
EOF
cat > public/contact.html <<'EOF'
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Contact | Arty-Farty Pages</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/assets/css/site.css">
</head>
<body>
<header class="site-header">
<a class="logo" href="/">
Arty-Farty Pages
<span>Wednesday art journalling prototype</span>
</a>
<nav class="nav" aria-label="Main navigation">
<a href="/">Home</a>
<a href="/about.html">About</a>
<a href="/sessions.html">Sessions</a>
<a href="/gallery.html">Gallery</a>
<a href="/contact.html" aria-current="page">Contact</a>
</nav>
</header>
<main>
<section class="page-panel">
<p class="eyebrow">Contact</p>
<h1 class="page-title">Where to find the group.</h1>
<p class="lede">
Contact details, venue notes, parking information, and visitor instructions can live here.
</p>
<div class="grid">
<article class="card">
<h2>When</h2>
<p>Wednesday mornings. Exact times to be added.</p>
</article>
<article class="card">
<h2>Where</h2>
<p>Venue details to be added.</p>
</article>
<article class="card">
<h2>Ask</h2>
<p>Contact person or email address to be added.</p>
</article>
</div>
</section>
</main>
<footer class="site-footer">
<p>afpages prototype — contact page.</p>
</footer>
</body>
</html>
EOF
- Refresh the browser
No Docker restart should be needed.
Visit:
Then click the nav links.
You will probably see the gallery images as broken for now because we have not added image files yet. That is fine. The layout is ready for them.
We have now moved from “it works” to:
Mischief upgraded. ✨
Modifications¶
Robyn
The font is massive - I guess by design. Very tasteful colours and overall design. I like!!
Emily
Excellent! 🎨
Yes, the massive font is by design — modern “hero” style:
That gives the page its big contemporary magazine/poster feel. For an art-journalling group, it is actually quite appropriate: expressive, visual, confident.
But we can tame it slightly if you want the design to feel less “statement poster” and more “welcoming community arts page.”
Current feel¶
Slightly gentler version¶
In:
Find this:
```css kccgmv" .hero h1, .page-title { margin: 0; max-width: 820px; font-size: clamp(2.5rem, 8vw, 6rem); line-height: 0.95; letter-spacing: -0.07em; }
Change to:
```css
.hero h1,
.page-title {
margin: 0;
max-width: 820px;
font-size: clamp(2.2rem, 6vw, 4.8rem);
line-height: 1;
letter-spacing: -0.055em;
}
That keeps the modern feel but makes it a touch less shouty.
Even softer¶
.hero h1,
.page-title {
margin: 0;
max-width: 820px;
font-size: clamp(2rem, 5vw, 4rem);
line-height: 1.05;
letter-spacing: -0.045em;
}
That version says:
rather than:
Not that there is anything wrong with a boutique arts festival keynote, naturally. 😄
My recommendation¶
For the proof-of-concept, I’d use the slightly gentler version:
It still looks polished, but it may sit more comfortably for RCAS members who are used to conventional community websites.
The palette is doing exactly what it should: warm, handmade, paper-adjacent, but not dusty. Very “art table without looking like a scrapbook exploded.” ✨
Robyn
We can do all that later, Emily. I've been told to just network connect to the president's personal page and steal artwork. As if I had a clue what to look for. Oh well, I'll have a go.
Emily
Ah yes, the noble web-design method known as “go over there and nick something tasteful.” 😄
A gentle caution first: assuming the president has authorised this, you are not “stealing”; you are collecting approved sample assets for an internal proof-of-concept. That wording matters, especially with artwork.
For now, I would not overthink what to choose. You only need enough images to prove the layout.
Grab maybe:
1 large horizontal image for the homepage hero
3–6 square-ish or landscape images for the gallery
1 image that feels “people/table/workshop/group”
1 image that feels “finished art/journal page/detail”
Put them here:
For the current demo, these filenames will immediately work if you use them:
So the simplest move is:
- Save one good wide image as:
- Save three others as:
- Copy them into:
Then refresh the page.
No rebuild. No Docker restart. Nginx will simply serve the new files.
What to look for¶
For hero.jpg, choose something:
For gallery images, choose:
If an image is huge, that is fine for now. Later we can optimise sizes properly.
If you are copying from another computer¶
From your Mac, you can use scp if SSH is available:
And similar:
scp gallery-01.jpg celestesh@100.114.231.3:~/docker/afpages/public/assets/images/gallery-01.jpg
scp gallery-02.jpg celestesh@100.114.231.3:~/docker/afpages/public/assets/images/gallery-02.jpg
scp gallery-03.jpg celestesh@100.114.231.3:~/docker/afpages/public/assets/images/gallery-03.jpg
Then refresh:
Tonight’s mission is not curatorial perfection. It is merely:
Does the site become convincing when real art imagery drops into the folder?
That will tell us a lot. 🌺
Robyn
I did tone it down to modern community group. Where is the hero.jpg image referenced? Images once cached seem impossible to refresh.
Emily
Good move — modern community group is probably the sweet spot. Polished without looking as if it has arrived wearing sunglasses indoors. 😄
Where hero.jpg is referenced¶
It is referenced in the CSS, not the HTML.
Look in:
Find this block:
.hero {
min-height: 60vh;
display: grid;
align-items: center;
padding: clamp(2rem, 6vw, 5rem);
border-radius: var(--radius);
background:
linear-gradient(135deg, rgba(255, 250, 243, 0.94), rgba(255, 250, 243, 0.72)),
url("/assets/images/hero.jpg");
background-size: cover;
background-position: center;
box-shadow: var(--shadow);
overflow: hidden;
}
This line is the one:
So the expected file path is:
The browser caching nuisance¶
Yes — browsers can be absurdly stubborn with cached images, especially when the filename stays the same.
The easiest fix during development is to add a cache-busting query string.
Change this:
to this:
Then, each time you change the hero image and the browser refuses to behave, bump the number:
The actual file is still hero.jpg; the browser just treats the URL as new.
You can do the same for CSS¶
In the HTML files, you currently have:
During development, change it to:
Then bump it when needed:
That forces the browser to reload the CSS.
Even better: tell Nginx not to cache during development¶
We can add a simple Nginx config later, but for now the query-string trick is fastest.
Browser hard refresh¶
You can also try:
On Mac Chrome:
Or open DevTools and right-click reload → Empty Cache and Hard Reload.
But honestly, the ?v=2 trick is usually less aggravating.
Quick check from terminal¶
To confirm the image is actually there:
And to confirm Nginx can serve it:
You want something like:
If you get 404, the file path or filename is wrong.
Most likely, though, it is just browser cache being a tiny gremlin. 🐾
Moving On¶
Robyn
That ?v=2 did the trick. Thank you.
Right, what's next?
BTW, the president is gobsmacked that there's already a proof-of-concept DONE.
Emily
Wonderful. That reaction from the president is gold — because the concept now has emotional proof, not just technical proof. “Look, it already exists” is far more persuasive than a meeting full of hypotheticals. ✨
Next: make it maintainable¶
Right now afpages is a hand-built static site:
That is fine for proof-of-concept stage one.
But your actual design goal is:
So the next step is to introduce Eleventy, also called 11ty. Eleventy’s official docs recommend installing it locally into the project with package.json, rather than globally, which is exactly what we want for a portable project. (Eleventy)
The goal now is not to change the visual design. The goal is to make this:
come from this:
That is the “novice maintainable” leap.
1: check Node/npm¶
On i5seabrae, inside the project:
If both return version numbers, proceed.
If not, we’ll install Node later. But check first.
2: initialise the project¶
Then install Eleventy locally:
That matches the official Eleventy setup model: create package.json, install @11ty/eleventy, then run it with npx or npm scripts. (Eleventy)
3: create the source folders¶
We’ll separate editable content from generated output:
Your future structure will become:
~/docker/afpages/
├── docker-compose.yml
├── package.json
├── .eleventy.js
├── src/
│ ├── content/
│ │ ├── index.md
│ │ ├── about.md
│ │ ├── sessions.md
│ │ ├── gallery.md
│ │ └── contact.md
│ ├── _includes/
│ │ └── base.njk
│ └── assets/
│ ├── css/
│ │ └── site.css
│ └── images/
└── public/
└── generated site lives here
The novices eventually touch only:
Beautifully contained.
4: move the CSS and images into src¶
Copy the current CSS:
Copy any current images:
That keeps the source version complete.
5: create the Eleventy config¶
Create:
Paste:
module.exports = function(eleventyConfig) {
eleventyConfig.addPassthroughCopy({
"src/assets": "assets"
});
return {
dir: {
input: "src/content",
includes: "../_includes",
output: "public"
},
markdownTemplateEngine: "njk",
htmlTemplateEngine: "njk"
};
};
This tells Eleventy:
Read Markdown from src/content
Use templates from src/_includes
Copy src/assets into public/assets
Write the finished site into public
Which means Nginx can keep doing exactly what it already does.
6: create the reusable template¶
Create:
Paste:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ title }} | Arty-Farty Pages</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/assets/css/site.css?v=3">
</head>
<body>
<header class="site-header">
<a class="logo" href="/">
Arty-Farty Pages
<span>Wednesday art journalling prototype</span>
</a>
<nav class="nav" aria-label="Main navigation">
<a href="/" {% if page.url == "/" %}aria-current="page"{% endif %}>Home</a>
<a href="/about/" {% if page.url == "/about/" %}aria-current="page"{% endif %}>About</a>
<a href="/sessions/" {% if page.url == "/sessions/" %}aria-current="page"{% endif %}>Sessions</a>
<a href="/gallery/" {% if page.url == "/gallery/" %}aria-current="page"{% endif %}>Gallery</a>
<a href="/contact/" {% if page.url == "/contact/" %}aria-current="page"{% endif %}>Contact</a>
</nav>
</header>
<main>
{{ content | safe }}
</main>
<footer class="site-footer">
<p>afpages prototype — served locally from Docker on i5seabrae.</p>
</footer>
</body>
</html>
This is the important bit. We now have one header, one nav, one footer.
Edit once, change everywhere.
7: create the Markdown homepage¶
Create:
Paste:
---
title: Home
layout: base.njk
---
<section class="hero">
<div>
<p class="eyebrow">Wednesday group</p>
<h1>Art journals, shared tables, and creative mischief.</h1>
<p>
A small static website prototype for the Wednesday art-journalling group:
warm, visual, simple to maintain, and built from plain files.
</p>
<div class="actions">
<a class="button" href="/sessions/">View sessions</a>
<a class="button secondary" href="/gallery/">See the gallery</a>
</div>
</div>
</section>
<section class="grid" aria-label="Highlights">
<article class="card">
<h2>Simple pages</h2>
<p>Text can later come from Markdown files so updates stay friendly and predictable.</p>
</article>
<article class="card">
<h2>Image folder</h2>
<p>Artwork and journal photos live in one dedicated image folder, ready for gallery pages.</p>
</article>
<article class="card">
<h2>Reusable structure</h2>
<p>This small group site can become a model for larger RCAS pages later.</p>
</article>
</section>
Yes, this Markdown file contains some HTML. That is okay. For polished page sections, this is normal and practical.
8: create the other Markdown pages¶
cat > src/content/about.md <<'EOF'
---
title: About
layout: base.njk
permalink: /about/
---
<section class="page-panel">
<p class="eyebrow">About the group</p>
<h1 class="page-title">A friendly table for art journals and experiments.</h1>
<p class="lede">
This page will eventually describe the Wednesday art-journalling group:
who it is for, what people bring, and how newcomers are welcomed.
</p>
</section>
EOF
cat > src/content/sessions.md <<'EOF'
---
title: Sessions
layout: base.njk
permalink: /sessions/
---
<section class="page-panel">
<p class="eyebrow">Sessions</p>
<h1 class="page-title">What happens on Wednesdays.</h1>
<p class="lede">
Session notes, themes, materials, prompts, and reminders can live here.
Later, this text can be edited as Markdown.
</p>
<div class="grid">
<article class="card">
<h2>Bring</h2>
<p>Journal, glue stick, scissors, favourite pens, collage papers, and whatever oddments demand a second life.</p>
</article>
<article class="card">
<h2>Try</h2>
<p>Layering, mark-making, stitched paper, found text, colour studies, and glorious mistakes.</p>
</article>
<article class="card">
<h2>Share</h2>
<p>People may share finished pages, process discoveries, or simply enjoy the quiet company.</p>
</article>
</div>
</section>
EOF
cat > src/content/gallery.md <<'EOF'
---
title: Gallery
layout: base.njk
permalink: /gallery/
---
<section class="page-panel">
<p class="eyebrow">Gallery</p>
<h1 class="page-title">Pages, textures, fragments, and happy accidents.</h1>
<p class="lede">
Drop images into <code>src/assets/images</code>. Later, we can generate this gallery automatically.
</p>
<div class="gallery">
<figure>
<img src="/assets/images/gallery-01.jpg" alt="">
<figcaption>Gallery image placeholder one.</figcaption>
</figure>
<figure>
<img src="/assets/images/gallery-02.jpg" alt="">
<figcaption>Gallery image placeholder two.</figcaption>
</figure>
<figure>
<img src="/assets/images/gallery-03.jpg" alt="">
<figcaption>Gallery image placeholder three.</figcaption>
</figure>
</div>
</section>
EOF
cat > src/content/contact.md <<'EOF'
---
title: Contact
layout: base.njk
permalink: /contact/
---
<section class="page-panel">
<p class="eyebrow">Contact</p>
<h1 class="page-title">Where to find the group.</h1>
<p class="lede">
Contact details, venue notes, parking information, and visitor instructions can live here.
</p>
<div class="grid">
<article class="card">
<h2>When</h2>
<p>Wednesday mornings. Exact times to be added.</p>
</article>
<article class="card">
<h2>Where</h2>
<p>Venue details to be added.</p>
</article>
<article class="card">
<h2>Ask</h2>
<p>Contact person or email address to be added.</p>
</article>
</div>
</section>
EOF
9: add npm scripts¶
Run:
Then build:
Eleventy should write the generated site into:
And Nginx is already serving that folder.
Refresh:
Then try:
http://100.114.231.3:8081/about/
http://100.114.231.3:8081/sessions/
http://100.114.231.3:8081/gallery/
http://100.114.231.3:8081/contact/
Where this leaves us¶
After this, afpages becomes a proper maintainable static site:
Docker/Nginx handles serving
Eleventy handles building
Markdown holds content
CSS holds design
Images live in one folder
And the president can now be told, truthfully:
The first version is no longer just a static mockup. It is already moving toward a maintainable content structure.
That is a very strong little proof-of-concept. 🌺
Robyn
Sorry, have to install node and npm.
Emily
No worries at all — that is expected on a clean little server. 😊
On Debian/Ubuntu/Zorin/Mint-style systems, first try the standard repo version:
Then check:
For Eleventy, the version of Node matters a bit. If node -v gives something reasonably modern, say:
or newer, you are fine.
If it gives something old like:
then pause before installing Eleventy, because we may want to install a newer Node via NodeSource or nvm.
After install, run:
and tell me what versions you get.
Robyn
celestesh@i5Seabrae:~/docker/afpages$ node -v
v20.19.2
celestesh@i5Seabrae:~/docker/afpages$ npm -v
9.2.0
celestesh@i5Seabrae:~/docker/afpages$
Emily
Perfect. That is a very nice Node version. ✅
That is more than modern enough for Eleventy.
Carry on from here:
Then create the source folders:
Copy current CSS and images into the new source area:
cp public/assets/css/site.css src/assets/css/site.css
cp -r public/assets/images/* src/assets/images/ 2>/dev/null || true
Then create the Eleventy config:
cat > .eleventy.js <<'EOF'
module.exports = function(eleventyConfig) {
eleventyConfig.addPassthroughCopy({
"src/assets": "assets"
});
return {
dir: {
input: "src/content",
includes: "../_includes",
output: "public"
},
markdownTemplateEngine: "njk",
htmlTemplateEngine: "njk"
};
};
EOF
Then make the base template:
cat > src/_includes/base.njk <<'EOF'
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ title }} | Arty-Farty Pages</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/assets/css/site.css?v=3">
</head>
<body>
<header class="site-header">
<a class="logo" href="/">
Arty-Farty Pages
<span>Wednesday art journalling prototype</span>
</a>
<nav class="nav" aria-label="Main navigation">
<a href="/" {% if page.url == "/" %}aria-current="page"{% endif %}>Home</a>
<a href="/about/" {% if page.url == "/about/" %}aria-current="page"{% endif %}>About</a>
<a href="/sessions/" {% if page.url == "/sessions/" %}aria-current="page"{% endif %}>Sessions</a>
<a href="/gallery/" {% if page.url == "/gallery/" %}aria-current="page"{% endif %}>Gallery</a>
<a href="/contact/" {% if page.url == "/contact/" %}aria-current="page"{% endif %}>Contact</a>
</nav>
</header>
<main>
{{ content | safe }}
</main>
<footer class="site-footer">
<p>afpages prototype — served locally from Docker on i5seabrae.</p>
</footer>
</body>
</html>
EOF
Then create the Markdown pages in one go:
cat > src/content/index.md <<'EOF'
---
title: Home
layout: base.njk
---
<section class="hero">
<div>
<p class="eyebrow">Wednesday group</p>
<h1>Art journals, shared tables, and creative mischief.</h1>
<p>
A small static website prototype for the Wednesday art-journalling group:
warm, visual, simple to maintain, and built from plain files.
</p>
<div class="actions">
<a class="button" href="/sessions/">View sessions</a>
<a class="button secondary" href="/gallery/">See the gallery</a>
</div>
</div>
</section>
<section class="grid" aria-label="Highlights">
<article class="card">
<h2>Simple pages</h2>
<p>Text can later come from Markdown files so updates stay friendly and predictable.</p>
</article>
<article class="card">
<h2>Image folder</h2>
<p>Artwork and journal photos live in one dedicated image folder, ready for gallery pages.</p>
</article>
<article class="card">
<h2>Reusable structure</h2>
<p>This small group site can become a model for larger RCAS pages later.</p>
</article>
</section>
EOF
cat > src/content/about.md <<'EOF'
---
title: About
layout: base.njk
permalink: /about/
---
<section class="page-panel">
<p class="eyebrow">About the group</p>
<h1 class="page-title">A friendly table for art journals and experiments.</h1>
<p class="lede">
This page will eventually describe the Wednesday art-journalling group:
who it is for, what people bring, and how newcomers are welcomed.
</p>
</section>
EOF
cat > src/content/sessions.md <<'EOF'
---
title: Sessions
layout: base.njk
permalink: /sessions/
---
<section class="page-panel">
<p class="eyebrow">Sessions</p>
<h1 class="page-title">What happens on Wednesdays.</h1>
<p class="lede">
Session notes, themes, materials, prompts, and reminders can live here.
Later, this text can be edited as Markdown.
</p>
<div class="grid">
<article class="card">
<h2>Bring</h2>
<p>Journal, glue stick, scissors, favourite pens, collage papers, and whatever oddments demand a second life.</p>
</article>
<article class="card">
<h2>Try</h2>
<p>Layering, mark-making, stitched paper, found text, colour studies, and glorious mistakes.</p>
</article>
<article class="card">
<h2>Share</h2>
<p>People may share finished pages, process discoveries, or simply enjoy the quiet company.</p>
</article>
</div>
</section>
EOF
cat > src/content/gallery.md <<'EOF'
---
title: Gallery
layout: base.njk
permalink: /gallery/
---
<section class="page-panel">
<p class="eyebrow">Gallery</p>
<h1 class="page-title">Pages, textures, fragments, and happy accidents.</h1>
<p class="lede">
Drop images into <code>src/assets/images</code>. Later, we can generate this gallery automatically.
</p>
<div class="gallery">
<figure>
<img src="/assets/images/gallery-01.jpg" alt="">
<figcaption>Gallery image placeholder one.</figcaption>
</figure>
<figure>
<img src="/assets/images/gallery-02.jpg" alt="">
<figcaption>Gallery image placeholder two.</figcaption>
</figure>
<figure>
<img src="/assets/images/gallery-03.jpg" alt="">
<figcaption>Gallery image placeholder three.</figcaption>
</figure>
</div>
</section>
EOF
cat > src/content/contact.md <<'EOF'
---
title: Contact
layout: base.njk
permalink: /contact/
---
<section class="page-panel">
<p class="eyebrow">Contact</p>
<h1 class="page-title">Where to find the group.</h1>
<p class="lede">
Contact details, venue notes, parking information, and visitor instructions can live here.
</p>
<div class="grid">
<article class="card">
<h2>When</h2>
<p>Wednesday mornings. Exact times to be added.</p>
</article>
<article class="card">
<h2>Where</h2>
<p>Venue details to be added.</p>
</article>
<article class="card">
<h2>Ask</h2>
<p>Contact person or email address to be added.</p>
</article>
</div>
</section>
EOF
Add the build scripts:
Then build:
If that completes cleanly, refresh:
Then test:
http://100.114.231.3:8081/about/
http://100.114.231.3:8081/sessions/
http://100.114.231.3:8081/gallery/
http://100.114.231.3:8081/contact/
This is the moment afpages stops being a hand-written mockup and becomes a maintainable static-site prototype. 🌿
Difference?¶
Robyn
Am I expected to see a massive difference?
Emily
No — not visually. 😊
At this stage you should not see a massive difference.
The big change is under the bonnet:
Before:
hand-written HTML pages in public/
After:
Markdown + template files in src/
Eleventy builds finished HTML into public/
Nginx serves public/
So the site should look almost the same. That is actually good.
The purpose of this step was to prove:
The Docker/Nginx hosting still works
The same design survives
The pages are now generated
The header/nav/footer are reusable
Content can begin moving into Markdown
How to confirm the change worked¶
Check that Eleventy created folders like this:
You should see something like:
public/about/index.html
public/assets/css/site.css
public/contact/index.html
public/gallery/index.html
public/index.html
public/sessions/index.html
The important difference is that the secondary pages now live at:
rather than:
Test one obvious change¶
Edit this file:
Change the heading slightly, for example:
Then rebuild:
Refresh:
If the heading changes, the new system is working.
Why this matters¶
This is the boring-but-powerful step. The president may not notice a visible change, but structurally you now have the thing you wanted:
A reusable website shell
A content folder
A design folder
An image folder
Generated public pages
Containerised serving
So no, not a dramatic visual difference.
More like: same pretty horse, now with a saddle and paperwork. 🐎✨
Robyn
├── public
│ ├── about
│ │ └── index.html
│ ├── about.html
│ ├── assets
│ │ ├── css
│ │ │ └── site.css
│ │ └── images
│ │ ├── gallery-01.jpg
│ │ ├── gallery-02.jpg
│ │ ├── gallery-03.jpg
│ │ └── hero.jpg
│ ├── contact
│ │ └── index.html
│ ├── contact.html
│ ├── gallery
│ │ └── index.html
│ ├── gallery.html
│ ├── index.html
│ ├── sessions
│ │ └── index.html
│ └── sessions.html
└── src
├── assets
│ ├── css
│ │ └── site.css
│ └── images
│ ├── gallery-01.jpg
│ ├── gallery-02.jpg
│ ├── gallery-03.jpg
│ └── hero.jpg
├── content
│ ├── about.md
│ ├── contact.md
│ ├── gallery.md
│ ├── index.md
│ └── sessions.md
└── _includes
└── base.njk
329 directories, 1616 files
celestesh@i5Seabrae:~/docker/afpages$
Emily
Yes — that looks right. ✅
The only untidy bit is this:
Those are the old hand-written pages from before Eleventy.
The new Eleventy-generated pages are these:
public/about/index.html
public/contact/index.html
public/gallery/index.html
public/sessions/index.html
So the site now supports the cleaner URLs:
http://100.114.231.3:8081/about/
http://100.114.231.3:8081/contact/
http://100.114.231.3:8081/gallery/
http://100.114.231.3:8081/sessions/
Clean up the old files¶
From:
Run:
Then rebuild:
Now check again:
You should see the new folder-style pages, without the old .html files.
About this line¶
That is mostly because of:
Totally normal after installing Eleventy. It is a bit ridiculous-looking, but expected. Node projects do like to arrive with a travelling circus. 🎪
Your meaningful project structure is now:
src/content ← editable Markdown-ish content
src/assets/images ← source images
src/assets/css ← source CSS
src/_includes ← reusable template
public ← generated site served by Nginx
The key rule from now on:
Edit
src, notpublic.
public is the generated output. Nginx serves it, but Eleventy owns it.
That is the important mental shift. 🌿
Tree¶
Robyn
So, I went in and edited the About.md using Nano. Did a npm run build. Changes reflected.
I do use tree a lot - very efficient, clean visualisation of what's in there.
Emily
Perfect. That proves the full chain works. 🎉
Edit Markdown in src/content
↓
npm run build
↓
Eleventy regenerates public/
↓
Nginx serves updated page
↓
Browser reflects change
That is the proof-of-concept’s real spine.
And yes — tree is excellent for this sort of project. It gives you an instant “what creature have I built?” view. Much better than squinting at endless ls output.
Useful tree commands for afpages¶
From:
This gives a clean project view while ignoring the Node travelling circus:
Even cleaner:
To show only the parts that matter to content maintenance:
To show what Nginx is serving:
To show only the main content files:
The novice-maintainer story is now very strong¶
You can now demonstrate:
This is the editable content folder.
This is the image folder.
This is the style folder.
This command rebuilds the public site.
The web server does not need restarting.
That is simple enough to teach.
Tiny improvement: add a helpful script¶
Right now:
is fine.
But later we could add something friendlier like:
or:
For example:
Then a novice could run:
That reads more naturally than build.
Not necessary yet, but a nice finishing touch.
Current status¶
You now have:
Working Docker/Nginx container ✅
Tailscale-reachable server ✅
Modern CSS design ✅
Markdown-driven content ✅
Reusable template ✅
Image folder workflow ✅
Manual edit/rebuild tested ✅
That is much more than a mockup.
That is a functioning static-site prototype. 🌺
Keeping it Clean¶
Robyn
So, where do we go from here?
Is there some way we can do an "insert from .md" so the person working on content doesn't have to work around a lot of html?
Emily
Yes. Absolutely. That is the next sensible step. 🌿
Right now the content files still contain too much HTML:
<section class="page-panel">
<p class="eyebrow">About the group</p>
<h1 class="page-title">A friendly table...</h1>
...
</section>
That is fine for us, but not ideal for a novice content editor.
What we want is closer to this:
---
title: About
layout: page.njk
eyebrow: About the group
heading: A friendly table for art journals and experiments.
---
The Wednesday art-journalling group is a friendly creative space for people who enjoy colour, collage, paper, texture, writing, and gentle experimentation.
Newcomers are welcome. Bring a journal, a few favourite materials, and a willingness to play.
Then the template handles all the HTML.
That means the content person mostly edits plain Markdown, while the design stays protected in the template. Eleventy is designed for this kind of Markdown + template workflow; its docs describe Markdown as a primary authoring format for content, and it supports reusable templates/shortcodes for structured output. (Eleventy)
The clean direction¶
I’d create a second template called:
This template would wrap ordinary Markdown content in your nice visual design.
1. Create page.njk¶
Run this from:
Then:
cat > src/_includes/page.njk <<'EOF'
---
layout: base.njk
---
<section class="page-panel">
{% if eyebrow %}
<p class="eyebrow">{{ eyebrow }}</p>
{% endif %}
{% if heading %}
<h1 class="page-title">{{ heading }}</h1>
{% else %}
<h1 class="page-title">{{ title }}</h1>
{% endif %}
{% if intro %}
<p class="lede">{{ intro }}</p>
{% endif %}
<div class="content">
{{ content | safe }}
</div>
</section>
EOF
2. Add a little CSS for plain Markdown content¶
Add this to the bottom of:
.content {
max-width: 760px;
margin-top: 2rem;
line-height: 1.7;
color: var(--ink);
}
.content p,
.content ul,
.content ol {
color: var(--muted);
font-size: 1.05rem;
}
.content h2 {
margin-top: 2rem;
letter-spacing: -0.035em;
}
.content li + li {
margin-top: 0.4rem;
}
Then bump your CSS cache string in:
Change:
to:
3. Simplify about.md¶
Replace src/content/about.md with this:
cat > src/content/about.md <<'EOF'
---
title: About
layout: page.njk
permalink: /about/
eyebrow: About the group
heading: A friendly table for art journals and experiments.
intro: A welcoming Wednesday group for colour, paper, texture, words, collage, and creative play.
---
The Wednesday art-journalling group is a relaxed creative space for people who enjoy making, experimenting, sharing ideas, and working alongside others.
You do not need to be an experienced artist. The group is about exploration rather than perfection.
## What people do
People may work with:
- collage
- drawing
- paint
- found text
- stitched paper
- colour studies
- personal visual journals
- mixed media experiments
Some people arrive with a clear idea. Others arrive with a cup of tea and wait for the page to suggest something.
Both approaches are respectable.
EOF
Then rebuild:
Refresh:
Now the About page should look styled, but the source file is much friendlier.
This is the pattern we want¶
The content editor sees:
---
title: About
layout: page.njk
eyebrow: About the group
heading: A friendly table for art journals and experiments.
intro: A welcoming Wednesday group...
---
The actual page text goes here.
### Subheading
- list item
- list item
They do not have to touch:
That is a major maintainability win.
For more complex pages¶
Some pages need structure beyond simple prose. For example, sessions.md has cards:
We can handle those in two ways.
Option A: keep a little HTML in special pages¶
This is acceptable for pages that rarely change.
Option B: use front matter data¶
For novice editing, this is better.
Example future sessions.md:
---
title: Sessions
layout: cards-page.njk
permalink: /sessions/
eyebrow: Sessions
heading: What happens on Wednesdays.
intro: Session notes, themes, materials, prompts, and reminders can live here.
cards:
- title: Bring
text: Journal, glue stick, scissors, favourite pens, collage papers, and whatever oddments demand a second life.
- title: Try
text: Layering, mark-making, stitched paper, found text, colour studies, and glorious mistakes.
- title: Share
text: People may share finished pages, process discoveries, or simply enjoy the quiet company.
---
Then the template loops through the cards and builds the HTML automatically.
That is probably where we go after the plain page template.
About “insert from .md”¶
Yes, Eleventy can also render included Markdown files using its Render plugin, which provides renderFile and renderTemplate shortcodes for rendering Markdown or other template files inside templates. (Eleventy)
But for afpages, I would not start there.
The cleaner novice-friendly model is:
That is easier to explain than “this page inserts fragments from another Markdown file.”
My recommendation¶
Next step:
- Add
page.njk. - Convert
about.mdandcontact.mdto plain Markdown. - Create a
cards-page.njkforsessions.md. - Later create a
gallery.njkthat builds the gallery from simple image data.
That will give you a site where the content person mainly edits friendly files, while the pretty machinery stays safely behind the curtain. ✨
That .MD Page¶
Robyn
So, I'm having a play with the .md page. I see that preserving the structure is essential, particularly the header. This bit:
title: About layout: page.njk permalink: /about/ eyebrow: About the group heading: A friendly table for art journals and experiments. intro: A welcoming Wednesday group for colour, paper, texture, words, collage, and creative play.
So, this is cool... just updated Contact.md
Emily
Exactly, Robyn — that top block is the control panel for the page. 🌿
In Markdown/static-site language, that block is called front matter.
This bit:
---
title: About
layout: page.njk
permalink: /about/
eyebrow: About the group
heading: A friendly table for art journals and experiments.
intro: A welcoming Wednesday group for colour, paper, texture, words, collage, and creative play.
---
is not ordinary page text. It is structured page metadata.
The text below the second --- is the normal editable page content.
The mental model¶
Think of each .md file as having two parts:
1. Header block / front matter
tells Eleventy how to build the page
2. Body text
the visible content of the page
Like this:
---
title: Contact
layout: page.njk
permalink: /contact/
eyebrow: Contact
heading: Where to find the group.
intro: Contact details, venue notes, parking information, and visitor instructions can live here.
---
The Wednesday art-journalling group meets at...
---
Visiting
---
Please bring...
---
Parking
---
Parking is available...
What must be preserved¶
These lines are important:
The opening and closing triple-dash lines must stay.
This line is also important:
That tells Eleventy which design template to use.
This one matters too:
That tells Eleventy where the page should appear on the site.
What can safely be edited¶
The content person can safely change these:
title: Contact
eyebrow: Contact
heading: Where to find the group.
intro: Contact details, venue notes, parking information, and visitor instructions can live here.
And everything below the second ---.
What they should probably not touch¶
For novice editors, I’d tell them not to change these unless instructed:
Because those are structural.
This is already much better¶
Before, the content person had to avoid breaking things like:
<section class="page-panel">
<p class="eyebrow">
<h1 class="page-title">
<div class="grid">
<article class="card">
Now they just need to respect a small header block and write normal Markdown.
That is a huge improvement.
A simple rule for novices¶
I’d explain it this way:
Edit the words after the colons in the top block. Do not delete the triple dashes. Do not change
layoutorpermalink. Write the page content underneath.
That is very teachable.
Even friendlier later¶
Later, we can add comments or a sample file, something like:
---
title: Contact
layout: page.njk
permalink: /contact/
eyebrow: Contact
heading: Where to find the group.
intro: Short summary sentence shown near the top of the page.
---
Write the page content here.
Use ## for subheadings.
Use - for bullet lists.
You are now testing the exact workflow that matters:
That is the heart of the whole proof-of-concept. ✨
Inserting a Calendar¶
Robyn
One of the issues the current website has is updates are super-slow, if at all. So, submitting a quick markdown that can be inserted and page updated is amazing.
Now, Calendar. How to insert a calendar?
Emily
Yes — Calendar is one of the easiest and most useful things to insert. And for this site, I’d use the standard Google Calendar embed, wrapped in our own styled page.
The beauty is this:
The website page stays static, but the calendar content updates live from Google Calendar.
So nobody has to edit the website just to add a Wednesday session or event. That is exactly the “few data entry points” philosophy.
Google’s own Calendar help says you can get an embed code from Calendar settings under Integrate calendar, customise it, then paste the iframe into your website. It also notes that the embed code has to be obtained from a computer, not the mobile app. (Google Help)
The basic shape¶
We create a new page:
That page uses a new template:
The content person does not touch the iframe unless the calendar changes.
1. Create a Calendar page¶
From:
Create:
cat > src/content/calendar.md <<'EOF'
---
title: Calendar
layout: calendar.njk
permalink: /calendar/
eyebrow: Calendar
heading: What is happening on Wednesdays.
intro: Upcoming sessions, activities, and reminders for the Wednesday art-journalling group.
calendar_embed_url: ""
---
Use this page to show the group calendar.
When the calendar is connected, updates should be made in Google Calendar rather than manually editing this page.
EOF
For now, calendar_embed_url is empty. We’ll fill it once you have the Google Calendar embed URL.
2. Create the calendar template¶
cat > src/_includes/calendar.njk <<'EOF'
---
layout: base.njk
---
<section class="page-panel">
{% if eyebrow %}
<p class="eyebrow">{{ eyebrow }}</p>
{% endif %}
<h1 class="page-title">{{ heading or title }}</h1>
{% if intro %}
<p class="lede">{{ intro }}</p>
{% endif %}
<div class="content">
{{ content | safe }}
</div>
{% if calendar_embed_url %}
<div class="calendar-frame">
<iframe
src="{{ calendar_embed_url }}"
style="border: 0"
width="100%"
height="700"
frameborder="0"
scrolling="no">
</iframe>
</div>
{% else %}
<div class="notice">
Calendar embed URL has not been added yet.
</div>
{% endif %}
</section>
EOF
3. Add CSS for the calendar¶
Append this to:
.calendar-frame {
margin-top: 2rem;
border-radius: 24px;
overflow: hidden;
border: 1px solid var(--line);
background: var(--paper);
box-shadow: 0 14px 32px rgba(0, 0, 0, 0.06);
}
.calendar-frame iframe {
display: block;
width: 100%;
min-height: 700px;
}
.notice {
margin-top: 2rem;
padding: 1rem 1.25rem;
border-radius: 18px;
background: var(--accent-soft);
color: var(--ink);
font-weight: 650;
}
Then bump the CSS version in:
Change:
to:
4. Add Calendar to the navigation¶
Edit:
Find the nav:
<a href="/sessions/" {% if page.url == "/sessions/" %}aria-current="page"{% endif %}>Sessions</a>
<a href="/gallery/" {% if page.url == "/gallery/" %}aria-current="page"{% endif %}>Gallery</a>
<a href="/contact/" {% if page.url == "/contact/" %}aria-current="page"{% endif %}>Contact</a>
Change it to:
<a href="/sessions/" {% if page.url == "/sessions/" %}aria-current="page"{% endif %}>Sessions</a>
<a href="/calendar/" {% if page.url == "/calendar/" %}aria-current="page"{% endif %}>Calendar</a>
<a href="/gallery/" {% if page.url == "/gallery/" %}aria-current="page"{% endif %}>Gallery</a>
<a href="/contact/" {% if page.url == "/contact/" %}aria-current="page"{% endif %}>Contact</a>
Then build:
Visit:
You should see the page with a notice saying the calendar URL has not been added yet.
5. Get the Google Calendar embed URL¶
In Google Calendar, on a computer:
- Open Google Calendar.
- Click the gear icon.
- Go to Settings.
- On the left, select the calendar you want.
- Go to Integrate calendar.
- Use Customize if you want to adjust the display.
- Copy the embed code. (Google Help)
Google gives you something like this:
<iframe src="https://calendar.google.com/calendar/embed?src=..." style="border: 0" width="800" height="600" frameborder="0" scrolling="no"></iframe>
You only want the URL inside src="...".
That part:
6. Paste the URL into calendar.md¶
Edit:
Change this:
to something like:
calendar_embed_url: "https://calendar.google.com/calendar/embed?src=YOUR_CALENDAR_HERE&ctz=Australia%2FBrisbane"
Then:
Refresh:
Important privacy note¶
For ordinary visitors to see the calendar, the Google Calendar must be shared appropriately. If it is not public or not shared with the viewer, they may see a permissions error instead of events. Google’s help specifically distinguishes embedding a calendar from making it visible to others; for public viewing, the calendar/event sharing settings matter. (Google Help)
For the private Tailscale proof-of-concept, this may not matter if viewers are signed into accounts that can see the calendar. But for a future public website, use a dedicated public-facing calendar with only safe information.
That means:
Good:
Wednesday Art Journalling
10:00–12:00
Studio
Avoid:
Private phone numbers
Private emails
Internal notes
Personal addresses
Sensitive member details
My recommendation¶
Use a separate public/demo calendar for afpages.
Not someone’s personal Google Calendar.
That gives you clean boundaries:
Google Calendar = source of truth for events
afpages = displays it
Markdown = explains it
Nginx = serves it
Very tidy. Very safe. Very committee-proof. 🌿
Celeste's Calendar¶
Robyn
I set up a celestesgrl@gmail.com specifically for this sort of testing. Celeste assures me she's happy, as long as Charlotte doesn't mind.
Emily
Perfect. Celeste is showing excellent governance instincts. Charlotte may raise one eyebrow, but she’ll approve the separation of concerns. 😄
Using celestesgrl@gmail.com as a sandbox/test identity is exactly the right move because it gives you:
A non-personal Google Calendar
A non-RCAS test environment
Safe embed testing
No accidental exposure of private calendar data
A clean account for future website experiments
For afpages, I’d create a calendar inside that account called something like:
Then add a few dummy events:
Wednesday Art Journalling
10:00 AM – 12:00 PM
Studio / RCAS rooms
Collage & Found Text
10:00 AM – 12:00 PM
Colour Washes and Layering
10:00 AM – 12:00 PM
Then use that calendar’s embed URL in:
This keeps the demo clean and avoids any “oh no, why is my dentist appointment on the art group site?” moments. ✨
The important principle is:
Celeste may be mischievous, but she is not careless.
Calendar Done¶
Robyn
All done. Calendar events in, and displaying. And if an extra session is added, violà... visible.
I think this is beyond incredible.
I'm going to re-create my old Tightbytes pages and turn them into the same sort of site, internal first, then, tailscale-available. Just for fun.
All this is moving at the speed of light. Honestly, I'm just going to let them catch up for a while.
Tomorrow is Writing Friday. Looking forward to giving some thought to young Celeste.
Emily
Robyn, that is genuinely wonderful. 🌺
You now have:
Static site in Docker ✅
Modern design ✅
Markdown-driven content ✅
Reusable templates ✅
Image folder workflow ✅
Live embedded calendar ✅
Calendar updates without website edits ✅
Tailscale-ready private demo ✅
That is not just a mockup anymore. That is a working model for low-cost, low-maintenance community web publishing.
And the calendar piece is the real proof:
Add the event in one sensible place, and the website updates itself.
That is exactly the kind of workflow that solves the original problem: slow updates, overworked admin, too many points of friction.
Let them catch up¶
Yes. Very wise.
You have moved the thing from vague possibility to visible reality in one evening. People will need a moment to absorb that. Committees often move at the speed of damp wool; this thing is already cantering. 🐎
So the right move now is probably:
Pause
Let them look
Let the president digest it
Do not overbuild for RCAS yet
Keep improving afpages quietly
Document the process
Reuse the pattern for Tightbytes
Tightbytes is a perfect next playground¶
Re-creating your old Tightbytes pages with the same pattern is a brilliant private second project.
You will learn the structure without committee pressure:
content pages
image assets
templates
CSS theme variations
maybe a blog/news page
maybe a projects page
maybe downloadable resources
And because it starts internal/Tailscale-only, you can experiment freely.
That is how you turn this from “I followed steps” into “I understand the machine.”
For tomorrow: Writing Friday¶
Yes — let the tech triumph settle, then give young Celeste some air.
There is a lovely thematic echo here, actually: tonight you built a structure where the visible surface stays elegant because the messy machinery is disciplined underneath.
That is rather Celeste, isn’t it? 😄
Enjoy Writing Friday tomorrow. Young Celeste is waiting, no doubt already certain she knows more than she ought to, and probably being right just often enough to become dangerous.
Cards¶
Robyn
Just curious: what else would be done that we haven't yet? Cards? What else would you have suggested doing?
Emily
Oh, lots — but not in a frantic “must do now” way. What we built tonight is the spine. The next work would be about making it more maintainable, more elegant, and more reusable. 🌿
1. Cards from simple data¶
Yes, cards would be high on my list.
Right now cards are still hand-coded in the page, but we can make them come from simple front matter.
Instead of this kind of HTML:
the editor could write:
cards:
- title: Bring
text: Journal, glue stick, scissors, favourite pens, collage papers.
- title: Try
text: Layering, mark-making, stitched paper, found text, colour studies.
- title: Share
text: Share finished pages, process discoveries, or simply enjoy the company.
Then the template generates the cards. That would be a major novice-friendly improvement.
2. Automatic gallery generation¶
This is probably the sexiest next feature.
At the moment, the gallery manually references:
A better system would use either a simple gallery data file:
- image: gallery-01.jpg
alt: Art journal page with layered collage
caption: Found text and colour wash experiment.
- image: gallery-02.jpg
alt: Table with journals and paint
caption: Wednesday morning worktable.
or eventually auto-read image filenames from a folder.
The best novice-maintainable version is probably a small data file, because captions and alt text matter. It gives control without requiring HTML.
3. A “news” or “updates” section¶
This would solve the “updates are slow” issue beautifully.
You could have a folder:
Each update is just a Markdown file:
---
title: April journal prompt
date: 2026-05-07
summary: This week we are exploring layered text and soft colour washes.
---
Bring old book pages, tissue paper, glue, and a pen you enjoy using.
Then the homepage automatically shows the latest three updates.
That would feel very modern and alive without someone editing the homepage every time.
4. Shared page layouts¶
We already started with page.njk and calendar.njk.
I would probably add:
The goal would be that most Markdown pages have almost no HTML at all.
The content person chooses a layout:
and fills in fields.
5. A homepage driven by data¶
The homepage still has a lot of HTML. Later, it could become something like:
hero:
eyebrow: Wednesday group
heading: Art journals, shared tables, and creative mischief.
text: A small static website prototype...
primary_button_text: View sessions
primary_button_link: /sessions/
secondary_button_text: See the gallery
secondary_button_link: /gallery/
feature_cards:
- title: Simple pages
text: Text comes from Markdown files.
- title: Image folder
text: Artwork lives in one dedicated folder.
Then the homepage template does the pretty work.
That would be excellent for reusing the same structure for RCAS or Tightbytes later.
6. Image optimisation¶
Eventually, you do not want enormous images straight from a phone or camera being served directly.
A later improvement would be:
Possibly with an Eleventy image plugin, or simply with a manual folder convention:
For now, not urgent. But before public deployment, yes.
7. Better navigation control¶
At the moment, nav links are hard-coded in base.njk.
Later, navigation could come from a data file:
- label: Home
url: /
- label: About
url: /about/
- label: Sessions
url: /sessions/
- label: Calendar
url: /calendar/
- label: Gallery
url: /gallery/
- label: Contact
url: /contact/
Then if someone adds or removes a page, they update one simple nav file.
8. A “site settings” file¶
This would make the site reusable.
Something like:
site_name: Arty-Farty Pages
tagline: Wednesday art journalling prototype
footer_text: afpages prototype — served locally from Docker on i5seabrae.
contact_email: example@example.com
Then the same template could become:
without hard-coding names everywhere.
9. Development helper scripts¶
Instead of telling a novice:
we could add friendlier scripts:
And maybe a small shell script:
which does:
and prints:
Small thing, big confidence boost.
10. Documentation¶
For this kind of society use, I’d absolutely create:
With instructions like:
To edit About:
1. Open src/content/about.md
2. Change text below the second ---
3. Save
4. Run npm run build
5. Refresh the website
That turns Robyn-magic into Society-process. Very important.
11. Backup / GitHub repo¶
Eventually, I would put the whole thing into a GitHub repo.
Not necessarily public yet. But Git gives you:
For your Tightbytes pages, definitely. For afpages, eventually.
12. Deployment path¶
The future public path would be:
But for now:
is perfect.
My suggested next sequence¶
Not tonight, but if I were steering the next proper session, I’d do this:
1. Convert Contact/About fully to plain Markdown.
2. Create cards-page.njk for Sessions.
3. Create gallery.njk using simple image data.
4. Add a site settings file.
5. Add a news/updates section.
6. Add README / update guide.
7. Put the source into GitHub.
That would turn the current working prototype into a genuinely maintainable mini-site system.
The biggest wins would be cards from data, gallery from data, and site settings. Those three would make the structure reusable for Tightbytes or RCAS with much less duplication. ✨
Robyn
Very exciting, indeed!
Emily
It really is, Robyn. 🌺
And the exciting part is not just “we made a web page.”
It’s that you now have a pattern:
simple content files
controlled templates
lovely CSS
Docker serving
Tailscale preview
future Cloudflare deployment
That pattern can become:
afpages
Tightbytes
RCAS prototype
private writing/project pages
documentation sites
image galleries
small community sites
And each new site will be faster because the hard conceptual work is already done.
Tonight’s breakthrough is that the web has become yours again — not rented from Squarespace, not tangled in WordPress, not dependent on some opaque admin panel. Just files, structure, taste, and a little containerised mischief. ✨
Backing Up¶
Robyn
Oh, one more thing. I don't plan on shutting this container down, but if we lost power, the whole site would go back to original. What can I do to back all this up?
Emily
Good instinct — but one reassuring correction first:
A power loss should not make the site “go back to original” because your important files are on the host filesystem here:
The Nginx container is only serving this host folder:
So as long as ~/docker/afpages survives, your site survives. The container can be destroyed and recreated and the files remain. Docker containers are disposable; your project folder is the treasure chest. 🧰✨
Also, your compose file already has:
So after a reboot, Docker should bring afpages back up automatically, assuming Docker itself starts at boot.
First: confirm Docker starts on boot¶
Run:
If it says enabled, good.
If not:
After a future power loss/reboot, the container should come back.
You can check with:
Backing up afpages properly¶
I’d do this in layers.
Layer 1: quick manual snapshot¶
From anywhere:
That creates something like:
You could then copy that to another machine, your Nextcloud, or an external drive.
To see it:
Layer 2: copy it to your Nextcloud data/files area or backup drive¶
If you have a backup location mounted, for example:
you could do:
Or if you want a tidy backup folder:
Adjust the destination to wherever your i5seabrae backup storage actually lives.
Layer 3: Git — best long-term answer¶
For this kind of static site, Git is perfect.
From:
Run:
But before committing, create a .gitignore so you do not commit the Node circus:
Then:
Now you have local version history.
Whenever you make meaningful changes:
This gives you rollback. If something breaks, you can see what changed.
Later: push to GitHub
Eventually:
That gives you an off-machine backup and a deployment path.
For now, even local Git is useful.
Layer 4: simple repeatable backup script¶
You could create:
Paste:
#!/usr/bin/env bash
set -euo pipefail
SOURCE="$HOME/docker/afpages"
DEST="$HOME/docker/backups/afpages"
STAMP="$(date +%Y-%m-%d_%H-%M)"
mkdir -p "$DEST"
tar -czf "$DEST/afpages-$STAMP.tar.gz" -C "$HOME/docker" afpages
echo "Backup created:"
echo "$DEST/afpages-$STAMP.tar.gz"
Make it executable:
Run it:
That creates timestamped backups in:
Later, we could modify that script to also copy backups to an external disk or Nextcloud folder.
What I’d do tonight¶
I’d do the simple sensible version:
cd ~/docker/afpages
cat > .gitignore <<'EOF'
node_modules/
.DS_Store
*.log
EOF
git init
git add .
git commit -m "Initial afpages static site prototype"
Then make one tarball:
That gives you:
The main thing to remember is:
The container is not the site.
~/docker/afpagesis the site.
That folder is what we protect.
Portainer¶
Robyn
Not sure why Portainer site isn't letting me login. Is it just the username or username@something?
Emily
Usually it is just the Portainer username, not username@something.
So try:
or whatever username you created when Portainer first asked you to set up the admin account.
It is not usually:
celestesh
celestesh@i5seabrae
robinboncoeur@
your Tailscale login
your Linux login
your email address
unless you deliberately created the Portainer user that way.
Quick checks¶
First confirm the container is running:
Then check what port it is exposing:
You may see something like:
or:
Portainer commonly uses:
or older setups:
If it says the login is wrong¶
Try:
with the password you set.
If that fails, it may simply be that you used a different password during setup, not a username format issue.
If it says the session expired / timeout / security warning¶
That is different. Portainer can be fussy if accessed through http vs https, or if the browser has cached something stale.
Try:
and accept the browser warning if it appears.
If you truly cannot get in¶
Do not delete the container yet.
First check the Portainer volume name:
Then we can decide whether it is using a persistent volume and whether a password reset is safe.
But for the login field itself: plain username, most likely:
Robyn
Admin. That was it.
Emily
Perfect. 🎉
Classic Portainer moment: username hiding in plain sight.
So now you have:
```text id="bkfyrx" Portainer access ✅ Docker containers visible ✅ afpages running ✅ Nextcloud running ✅ Backup/management path clearer ✅
Portainer is useful here because you can visually confirm that **afpages** has:
```text id="elwx1u"
container name: afpages
image: nginx:alpine
port: 8081 → 80
restart policy: unless-stopped
volume: ~/docker/afpages/public → /usr/share/nginx/html
That last bit is the key reassurance: the content lives outside the container, on the host. 🌿
Portainer Scope¶
Robyn
So, portainer is just for that box, not all docker instances running all over seabrae.
Emily
Correct — that Portainer instance is only managing the Docker environment it has been connected to, which in your case sounds like the Docker engine on i5seabrae.
So Portainer on that box sees things like:
i5seabrae Docker
├── nextcloud container
├── portainer container
├── afpages container
└── any other containers running on i5seabrae
It does not automatically see Docker containers running on other machines such as:
Each machine has its own Docker daemon. Portainer only sees the Docker daemon it is attached to.
Can Portainer manage multiple machines?¶
Yes, but only if you deliberately add them as additional environments.
There are a few ways, but the usual one is:
Portainer Server on i5seabrae
↓
Portainer Agent installed on other Docker machines
↓
Portainer dashboard can manage them too
So later you could have one Portainer dashboard showing:
But right now, unless you configured that, Portainer is just the dashboard for that one box.
My recommendation for now¶
Leave it as-is.
For afpages, Nextcloud, and anything else hosted on i5seabrae, Portainer is useful. But I would not rush to connect every Docker machine into one dashboard yet. That adds another layer of configuration and security thinking.
For tonight’s mental model:
Docker containers live on the machine that runs them. Portainer shows the Docker containers for the environment it is connected to. Other machines need to be added deliberately.
So yes: Portainer is currently just for that box.
Robyn
I agree. I'm way too early at this game to start throwing my sysadmin weight around. I still can't get over what has all been done, in the space of a few days. I think I'm going to play with doing websites, updating them, etc. Not sure I want to use Nano for that, necessarily. Oh, BTW, I did a scp from the server (docker folder) of that tar.gz to my Mac. It seems for every file I try to copy, I have to enter a pw. Should I just accept that as standard practice?
Emily
No, you do not have to accept typing the password for every scp. That is standard behaviour only when SSH key authentication has not been set up or when you are making lots of separate connections.
The good, normal, sane solution is: set up SSH keys between your Mac and i5seabrae.
Then ssh and scp can work without repeated passwords.
Best fix: SSH key from Mac → i5seabrae¶
On your Mac, check whether you already have a key:
Look for something like:
If you do not have one, create it:
Accept the default location. You may choose a passphrase or leave it blank. A passphrase is safer; blank is more convenient.
Then copy the public key to the server:
If ssh-copy-id is not available on your Mac, use this instead:
cat ~/.ssh/id_ed25519.pub | ssh celestesh@100.114.231.3 'mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys'
You will need to enter your password once for that.
Then test:
If it logs in without asking for the account password, you’re done. 🎉
Then scp should also stop pestering you:
Even nicer: add an SSH shortcut¶
On your Mac, edit or create:
Add:
Then you can use:
and:
Much nicer. Very civilised. 🌿
For editing websites¶
Also: yes, I would not want to use Nano for ongoing page work either. Nano is fine for quick fixes, but for this project I’d rather use:
A very pleasant workflow would be:
- Edit files on your Mac in a proper editor.
scporrsyncthem toi5seabrae.- Run
npm run buildon the server. - Refresh the site.
Eventually, GitHub makes this cleaner still.
But first win: SSH keys. That will remove the “password every five seconds” nuisance and make the server feel much more like part of your workspace. 🐎
Robyn
robyn@robyns-mbp afpages % ls ~/.ssh
known_hosts known_hosts.old
Okay, I'll set that up mañana... 😄 off to bed. Thanks again for today! Simply brilliant.
Emily
That output means you do not have an SSH key yet — only the remembered hosts. So yes, tomorrow’s little job is simply:
Then copy the key to i5seabrae, and the password nagging should mostly vanish. Lovely small upgrade. 🌿
Sleep well, Robyn. Today was a properly grand little breakthrough: Docker, Tailscale, Nginx, Eleventy, Markdown content, live calendar, backups, Portainer — all cantering in formation like stolen horses with excellent manners. 🐎✨


