Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c6e807653 | ||
|
|
0725d3eb1a | ||
|
|
bb8fd62041 | ||
|
|
1745ea9e93 | ||
|
|
2f1bb6deb5 | ||
|
|
40d5b54055 | ||
|
|
e2934f84b8 | ||
|
|
a4069d27e4 | ||
|
|
b33a72ced0 | ||
|
|
dde33d0907 | ||
|
|
fbf3dbe33e | ||
|
|
21785ed493 | ||
|
|
557fa92e1f | ||
|
|
9e730bbcfd | ||
|
|
05593346fc | ||
|
|
7bac8b4011 | ||
|
|
03b124f295 | ||
|
|
8f1d545ab4 | ||
|
|
f5cfa74dde | ||
|
|
d3d2421ac3 | ||
|
|
428398820c | ||
|
|
3ad2c47bb3 | ||
|
|
b9c7791f95 | ||
|
|
35c513d08b | ||
|
|
1a264905ea | ||
|
|
933720125a |
@@ -6,7 +6,7 @@ This is the static website for Protospace, a non-profit makerspace in Calgary, A
|
|||||||
|
|
||||||
## Source of truth
|
## Source of truth
|
||||||
|
|
||||||
- Edit the HTML, CSS, and metadata files directly.
|
- Edit the HTML, CSS, and metadata files directly under `public/`.
|
||||||
- Do not create or reintroduce a static-site generator.
|
- Do not create or reintroduce a static-site generator.
|
||||||
- Do not add a build script that overwrites manually maintained pages.
|
- Do not add a build script that overwrites manually maintained pages.
|
||||||
- There is intentionally no `build.js` or equivalent generator.
|
- There is intentionally no `build.js` or equivalent generator.
|
||||||
@@ -14,9 +14,10 @@ This is the static website for Protospace, a non-profit makerspace in Calgary, A
|
|||||||
|
|
||||||
## Page structure
|
## Page structure
|
||||||
|
|
||||||
- Every page must be an `index.html` inside its own route directory:
|
- Every page must be an `index.html` inside its own route directory under `public/`:
|
||||||
- `/index.html`
|
- `/index.html`
|
||||||
- `/the-space/index.html`
|
- `/the-space/index.html`
|
||||||
|
- `/tools/index.html`
|
||||||
- `/join-us/index.html`
|
- `/join-us/index.html`
|
||||||
- `/contact/index.html`
|
- `/contact/index.html`
|
||||||
- `/wood-shop/index.html`
|
- `/wood-shop/index.html`
|
||||||
@@ -66,7 +67,7 @@ Keep source code pleasant for humans to read and edit.
|
|||||||
|
|
||||||
When adding or editing a page, match the existing structure:
|
When adding or editing a page, match the existing structure:
|
||||||
|
|
||||||
- Header with Protospace logo and links to About, Membership, Wiki, Portal, and Contact
|
- Header with Protospace logo and links to Spaces, Tools, Membership, Contact, Portal, and Wiki
|
||||||
- Main page content with one meaningful `<h1>`
|
- Main page content with one meaningful `<h1>`
|
||||||
- Sidebar containing the current open-house information, address, parking details, map iframe, and search control where applicable
|
- Sidebar containing the current open-house information, address, parking details, map iframe, and search control where applicable
|
||||||
- Footer containing YouTube, FAQ, and Facebook links
|
- Footer containing YouTube, FAQ, and Facebook links
|
||||||
@@ -82,7 +83,7 @@ If this information changes, update the sidebar, relevant page copy, JSON-LD, an
|
|||||||
|
|
||||||
## Assets
|
## Assets
|
||||||
|
|
||||||
- Store local images in `/assets/`.
|
- Store local images in `public/assets/`.
|
||||||
- Use descriptive filenames rather than WordPress upload names such as `photo_6_2023-06-06_16-39-08.jpg`.
|
- Use descriptive filenames rather than WordPress upload names such as `photo_6_2023-06-06_16-39-08.jpg`.
|
||||||
- Remove unused downloaded image variants when it is safe to do so.
|
- Remove unused downloaded image variants when it is safe to do so.
|
||||||
- Update every reference when renaming or removing an asset.
|
- Update every reference when renaming or removing an asset.
|
||||||
@@ -170,6 +171,7 @@ Before finishing an HTML change:
|
|||||||
- Run structural validation when available, for example:
|
- Run structural validation when available, for example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
cd public/
|
||||||
npx html-validate \
|
npx html-validate \
|
||||||
--rule doctype-style:off \
|
--rule doctype-style:off \
|
||||||
--rule void-style:off \
|
--rule void-style:off \
|
||||||
@@ -183,6 +185,7 @@ Before finishing an HTML change:
|
|||||||
- Start a local static server and smoke-test every route:
|
- Start a local static server and smoke-test every route:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
cd public/
|
||||||
python3 -m http.server 8765
|
python3 -m http.server 8765
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -19,9 +19,10 @@ The Protospace WordPress site has been replaced with static HTML and CSS pages.
|
|||||||
git checkout -b update-page
|
git checkout -b update-page
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Edit the HTML pages, `styles.css`, or files in `assets/`. Preview the site locally with any static web server, for example:
|
4. Edit the HTML pages, `public/styles.css`, or files in `public/assets/`. Preview the site locally from the public directory with any static web server, for example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
cd public/
|
||||||
python3 -m http.server
|
python3 -m http.server
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 265 KiB |
|
Before Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 289 KiB |
|
Before Width: | Height: | Size: 231 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 308 KiB |
|
Before Width: | Height: | Size: 232 KiB |
|
Before Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 231 KiB |
|
Before Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 256 KiB |
|
Before Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 307 KiB |
|
Before Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 230 KiB |
|
Before Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 76 KiB |
|
Before Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 211 KiB |
@@ -0,0 +1,159 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Export the Protospace Wiki tool catalog and its thumbnails.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 export_tools.py
|
||||||
|
|
||||||
|
The wiki uses a small JavaScript challenge. The equivalent cookie is sent on
|
||||||
|
both the page request and each thumbnail request.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from html.parser import HTMLParser
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import urljoin, urlparse
|
||||||
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
|
WIKI_URL = "https://wiki.protospace.ca/Tools_we_have"
|
||||||
|
WIKI_ROOT = "https://wiki.protospace.ca"
|
||||||
|
COOKIE = "human_check=verified"
|
||||||
|
ROOT = Path(__file__).resolve().parent
|
||||||
|
PUBLIC_ROOT = ROOT / "public"
|
||||||
|
OUTPUT = PUBLIC_ROOT / "tools.json"
|
||||||
|
IMAGE_DIR = PUBLIC_ROOT / "assets" / "tools"
|
||||||
|
|
||||||
|
|
||||||
|
class ToolsParser(HTMLParser):
|
||||||
|
"""Extract ordered category/gallery data from the MediaWiki HTML."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__(convert_charrefs=True)
|
||||||
|
self.category: str | None = None
|
||||||
|
self.in_h2 = False
|
||||||
|
self.h2_text: list[str] = []
|
||||||
|
self.item: dict | None = None
|
||||||
|
self.item_depth = 0
|
||||||
|
self.anchor: dict | None = None
|
||||||
|
self.items: list[dict] = []
|
||||||
|
|
||||||
|
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
||||||
|
attrs_dict = dict(attrs)
|
||||||
|
if tag == "h2":
|
||||||
|
self.in_h2 = True
|
||||||
|
self.h2_text = []
|
||||||
|
elif tag == "li" and "gallerybox" in (attrs_dict.get("class") or "").split():
|
||||||
|
self.item = {"category": self.category, "anchors": [], "src": None}
|
||||||
|
self.item_depth = 1
|
||||||
|
elif self.item is not None and tag == "li":
|
||||||
|
self.item_depth += 1
|
||||||
|
elif self.item is not None and tag == "a":
|
||||||
|
self.anchor = {"href": attrs_dict.get("href"), "text": []}
|
||||||
|
elif self.item is not None and tag == "img" and self.item["src"] is None:
|
||||||
|
self.item["src"] = attrs_dict.get("src")
|
||||||
|
|
||||||
|
def handle_startendtag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
||||||
|
self.handle_starttag(tag, attrs)
|
||||||
|
self.handle_endtag(tag)
|
||||||
|
|
||||||
|
def handle_endtag(self, tag: str) -> None:
|
||||||
|
if tag == "h2" and self.in_h2:
|
||||||
|
category = " ".join("".join(self.h2_text).split())
|
||||||
|
if category and category.lower() != "contents":
|
||||||
|
self.category = category
|
||||||
|
self.in_h2 = False
|
||||||
|
elif tag == "a" and self.anchor is not None:
|
||||||
|
self.anchor["text"] = " ".join("".join(self.anchor["text"]).split())
|
||||||
|
if self.item is not None:
|
||||||
|
self.item["anchors"].append(self.anchor)
|
||||||
|
self.anchor = None
|
||||||
|
elif tag == "li" and self.item is not None:
|
||||||
|
self.item_depth -= 1
|
||||||
|
if self.item_depth == 0:
|
||||||
|
if self.item["category"] and self.item["category"].lower() != "removed equipment":
|
||||||
|
self.items.append(self.item)
|
||||||
|
self.item = None
|
||||||
|
|
||||||
|
def handle_data(self, data: str) -> None:
|
||||||
|
if self.in_h2:
|
||||||
|
self.h2_text.append(data)
|
||||||
|
if self.anchor is not None:
|
||||||
|
self.anchor["text"].append(data)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(url: str) -> tuple[bytes, str]:
|
||||||
|
request = Request(url, headers={"Cookie": COOKIE, "User-Agent": "Protospace tools exporter/1.0"})
|
||||||
|
with urlopen(request, timeout=30) as response:
|
||||||
|
content_type = response.headers.get_content_type()
|
||||||
|
return response.read(), content_type
|
||||||
|
|
||||||
|
|
||||||
|
def image_filename(src: str) -> str:
|
||||||
|
parts = [part for part in urlparse(src).path.split("/") if part]
|
||||||
|
# MediaWiki thumbnails end with e.g. 120px-182.jpg; the preceding part is
|
||||||
|
# the original filename and is stable even when thumbnail dimensions change.
|
||||||
|
if len(parts) >= 2:
|
||||||
|
return parts[-2]
|
||||||
|
return Path(parts[-1]).name
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
try:
|
||||||
|
page_bytes, content_type = fetch(WIKI_URL)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Could not fetch {WIKI_URL}: {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
if content_type != "text/html" or b"Verifying you are human" in page_bytes:
|
||||||
|
print("The wiki returned the human-verification page; check the cookie.", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
parser = ToolsParser()
|
||||||
|
parser.feed(page_bytes.decode("utf-8", errors="replace"))
|
||||||
|
IMAGE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
exported: list[dict[str, str]] = []
|
||||||
|
skipped = 0
|
||||||
|
|
||||||
|
for raw in parser.items:
|
||||||
|
anchors = raw["anchors"]
|
||||||
|
name_anchor = next((a for a in anchors if "_ID:" in (a["href"] or "") or " ID:" in a["text"]), None)
|
||||||
|
name = name_anchor["text"] if name_anchor else ""
|
||||||
|
id_match = re.search(r"\bID:(\d+)\s*$", name)
|
||||||
|
photo_src = raw["src"]
|
||||||
|
if not name or not id_match or not photo_src:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
photo_url = urljoin(WIKI_ROOT, photo_src)
|
||||||
|
source_filename = image_filename(photo_url)
|
||||||
|
filename = f"{id_match.group(1)}{Path(source_filename).suffix.lower()}"
|
||||||
|
destination = IMAGE_DIR / filename
|
||||||
|
try:
|
||||||
|
image_bytes, image_type = fetch(photo_url)
|
||||||
|
if not image_type.startswith("image/") or not image_bytes:
|
||||||
|
raise ValueError(f"response was {image_type}, not an image")
|
||||||
|
destination.write_bytes(image_bytes)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Skipping {name}: broken photo ({exc})", file=sys.stderr)
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
link = urljoin(WIKI_ROOT, name_anchor["href"])
|
||||||
|
exported.append(
|
||||||
|
{
|
||||||
|
"ID": id_match.group(1),
|
||||||
|
"name": re.sub(r"\s*ID:\d+\s*$", "", name).strip(),
|
||||||
|
"photo file": f"/assets/tools/{filename}",
|
||||||
|
"link": link,
|
||||||
|
"category": raw["category"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
OUTPUT.write_text(json.dumps(exported, ensure_ascii=False, indent=2) + "\n")
|
||||||
|
print(f"Exported {len(exported)} tools to {OUTPUT}")
|
||||||
|
print(f"Skipped {skipped} tools without valid metadata or photos")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<meta property="og:description" content="Explore Protospace’s 3D printer village in Calgary, with filament and resin printers available to trained members." />
|
<meta property="og:description" content="Explore Protospace’s 3D printer village in Calgary, with filament and resin printers available to trained members." />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content="https://protospace.ca/3d-printers/" />
|
<meta property="og:url" content="https://protospace.ca/3d-printers/" />
|
||||||
<meta property="og:image" content="https://protospace.ca/assets/3d-printers-1-1024x768.jpg" />
|
<meta property="og:image" content="https://protospace.ca/assets/3d-printers-1-800.jpg" />
|
||||||
<meta property="og:site_name" content="Protospace" />
|
<meta property="og:site_name" content="Protospace" />
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<title>3D Printers – Protospace</title>
|
<title>3D Printers – Protospace</title>
|
||||||
@@ -68,11 +68,12 @@
|
|||||||
<div class="nav-wrap">
|
<div class="nav-wrap">
|
||||||
<a class="brand" href="/"><img width="360" height="70" src="/assets/logo-white.svg" alt="Protospace" /></a>
|
<a class="brand" href="/"><img width="360" height="70" src="/assets/logo-white.svg" alt="Protospace" /></a>
|
||||||
<nav class="nav">
|
<nav class="nav">
|
||||||
<a href="/the-space/">About</a>
|
<a href="/the-space/">Spaces</a>
|
||||||
|
<a href="/tools/">Tools</a>
|
||||||
<a href="/join-us/">Membership</a>
|
<a href="/join-us/">Membership</a>
|
||||||
<a href="https://wiki.protospace.ca">Wiki</a>
|
|
||||||
<a href="https://my.protospace.ca">Portal</a>
|
|
||||||
<a href="/contact/">Contact</a>
|
<a href="/contact/">Contact</a>
|
||||||
|
<a href="https://my.protospace.ca" class="external">Portal</a>
|
||||||
|
<a href="https://wiki.protospace.ca" class="external">Wiki</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -83,26 +84,24 @@
|
|||||||
<div class="entry">
|
<div class="entry">
|
||||||
<figure>
|
<figure>
|
||||||
<picture>
|
<picture>
|
||||||
<source type="image/webp" srcset="/assets/3d-printers-1-1024x768.webp 1024w, /assets/3d-printers-1.webp 1280w" sizes="(max-width: 760px) calc(100vw - 24px), 750px" />
|
<source type="image/webp" srcset="/assets/3d-printers-1-400.webp 400w, /assets/3d-printers-1-800.webp 800w, /assets/3d-printers-1-1600.webp 1600w" sizes="(max-width: 760px) calc(100vw - 24px), 750px" />
|
||||||
<img width="1024" height="768" src="/assets/3d-printers-1-1024x768.jpg" srcset="/assets/3d-printers-1-1024x768.jpg 1024w, /assets/3d-printers-1.jpg 1280w" sizes="(max-width: 760px) calc(100vw - 24px), 750px" alt="3D printer in the printer village" decoding="async" fetchpriority="high" />
|
<img width="800" height="602" src="/assets/3d-printers-1-800.jpg" srcset="/assets/3d-printers-1-400.jpg 400w, /assets/3d-printers-1-800.jpg 800w, /assets/3d-printers-1-1600.jpg 1600w" sizes="(max-width: 760px) calc(100vw - 24px), 750px" alt="Resin 3D printers and printing supplies" fetchpriority="high" decoding="async" />
|
||||||
</picture>
|
</picture>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<figure>
|
<figure>
|
||||||
<picture>
|
<picture>
|
||||||
<source type="image/webp" srcset="/assets/3d-printers-2-1024x768.webp 1024w, /assets/3d-printers-2.webp 1280w" sizes="(max-width: 760px) calc(100vw - 24px), 750px" />
|
<source type="image/webp" srcset="/assets/3d-printers-2-400.webp 400w, /assets/3d-printers-2-800.webp 800w, /assets/3d-printers-2-1600.webp 1600w" sizes="(max-width: 760px) calc(100vw - 24px), 750px" />
|
||||||
<img width="1024" height="768" src="/assets/3d-printers-2-1024x768.jpg" srcset="/assets/3d-printers-2-1024x768.jpg 1024w, /assets/3d-printers-2.jpg 1280w" sizes="(max-width: 760px) calc(100vw - 24px), 750px" alt="3D printer equipment" loading="lazy" decoding="async" />
|
<img width="800" height="602" src="/assets/3d-printers-2-800.jpg" srcset="/assets/3d-printers-2-400.jpg 400w, /assets/3d-printers-2-800.jpg 800w, /assets/3d-printers-2-1600.jpg 1600w" sizes="(max-width: 760px) calc(100vw - 24px), 750px" alt="3D printer village with filament and resin printers" loading="lazy" decoding="async" />
|
||||||
</picture>
|
</picture>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<figure>
|
<figure>
|
||||||
<picture>
|
<picture>
|
||||||
<source type="image/webp" srcset="/assets/3d-printers-3-1024x768.webp 1024w, /assets/3d-printers-3.webp 1280w" sizes="(max-width: 760px) calc(100vw - 24px), 750px" />
|
<source type="image/webp" srcset="/assets/3d-printers-3-400.webp 400w, /assets/3d-printers-3-800.webp 800w, /assets/3d-printers-3-1600.webp 1600w" sizes="(max-width: 760px) calc(100vw - 24px), 750px" />
|
||||||
<img width="1024" height="768" src="/assets/3d-printers-3-1024x768.jpg" srcset="/assets/3d-printers-3-1024x768.jpg 1024w, /assets/3d-printers-3.jpg 1280w" sizes="(max-width: 760px) calc(100vw - 24px), 750px" alt="3D printer in operation" loading="lazy" decoding="async" />
|
<img width="800" height="602" src="/assets/3d-printers-3-800.jpg" srcset="/assets/3d-printers-3-400.jpg 400w, /assets/3d-printers-3-800.jpg 800w, /assets/3d-printers-3-1600.jpg 1600w" sizes="(max-width: 760px) calc(100vw - 24px), 750px" alt="Desktop 3D printers and print supplies" loading="lazy" decoding="async" />
|
||||||
</picture>
|
</picture>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<p></p>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
@@ -112,7 +111,7 @@
|
|||||||
Open House every Tuesday from 7-9 pm (even holidays). Just drop in for a tour!
|
Open House every Tuesday from 7-9 pm (even holidays). Just drop in for a tour!
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="https://youtu.be/B9w5G6Rn99A">Watch the Protospace mini tour video</a>
|
<a href="https://youtu.be/B9w5G6Rn99A" class="external">Watch the Protospace mini tour video</a>
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
@@ -131,11 +130,11 @@
|
|||||||
<footer>
|
<footer>
|
||||||
<div class="footer-inner">
|
<div class="footer-inner">
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<div><a href="https://www.youtube.com/user/calgaryprotospace/playlists">YouTube</a></div>
|
<div><a href="https://www.youtube.com/user/calgaryprotospace/playlists" class="external">YouTube</a></div>
|
||||||
<div><a href="https://wiki.protospace.ca/Faq">F.A.Q.</a></div>
|
<div><a href="https://github.com/Protospace" class="external">GitHub</a></div>
|
||||||
<div><a href="https://www.facebook.com/CalgaryProtospace/">Facebook</a></div>
|
<div><a href="https://www.facebook.com/CalgaryProtospace/" class="external">Facebook</a></div>
|
||||||
</div>
|
</div>
|
||||||
<p>PROTOSPACE 2026 · All rights reserved</p>
|
<p>© PROTOSPACE 2026 · <a href="https://github.com/Protospace/main-website" class="external">Source Code</a></p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</body>
|
</body>
|
||||||
|
After Width: | Height: | Size: 461 KiB |
|
After Width: | Height: | Size: 265 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 439 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 438 KiB |
|
After Width: | Height: | Size: 235 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 130 KiB |