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 | ||
|
|
07056bd10b | ||
|
|
34625ceead | ||
|
|
a4197cc050 | ||
|
|
abdb760aaf | ||
|
|
92c8906f24 | ||
|
|
e1b3056b25 | ||
|
|
b52eecdd1c | ||
|
|
dc18f437ae | ||
|
|
07dcf2b49d | ||
|
|
328e9ca8de | ||
|
|
9ef29ca0ab | ||
|
|
445549b9ec | ||
|
|
fd49cd15ab | ||
|
|
fb5fcf2f2b | ||
|
|
d4a810ca05 | ||
|
|
5794154487 | ||
|
|
a0c0d8f4dc | ||
|
|
ce14df4b53 | ||
|
|
82cfb0440d | ||
|
|
264bf8c01f | ||
|
|
44082bdab6 |
@@ -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,13 +83,31 @@ 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.
|
||||||
- Do not link page content to the old WordPress upload paths.
|
- Do not link page content to the old WordPress upload paths.
|
||||||
- Give every image a useful `alt` attribute. Use `alt=""` only for genuinely decorative images.
|
- Give every image a useful `alt` attribute. Use `alt=""` only for genuinely decorative images.
|
||||||
- Keep image dimensions or CSS aspect ratios where possible to reduce layout shift.
|
- Keep image dimensions or CSS aspect ratios where possible to reduce layout shift.
|
||||||
|
- For every JPEG photo used in page content, generate a matching WebP version and serve it with the original JPEG as the fallback in a `<picture>` element:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<picture>
|
||||||
|
<source
|
||||||
|
type="image/webp"
|
||||||
|
srcset="/assets/example.webp 640w"
|
||||||
|
sizes="(max-width: 760px) 100vw, 750px"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
width="640"
|
||||||
|
height="480"
|
||||||
|
src="/assets/example.jpg"
|
||||||
|
alt="Descriptive photo description"
|
||||||
|
/>
|
||||||
|
</picture>
|
||||||
|
```
|
||||||
|
- Keep the WebP and fallback `srcset` candidates equivalent, preserve the fallback image's `alt`, dimensions, loading behavior, and decoding attributes, and verify that every referenced variant exists.
|
||||||
|
|
||||||
## Accessibility
|
## Accessibility
|
||||||
|
|
||||||
@@ -152,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 \
|
||||||
@@ -165,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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Protospace Website
|
||||||
|
|
||||||
|
The Protospace WordPress site has been replaced with static HTML and CSS pages. Static files improve security, reduce maintenance, and provide faster page loads without a database or server-side CMS.
|
||||||
|
|
||||||
|
## Editing the site
|
||||||
|
|
||||||
|
1. Open the [Protospace repository](https://github.com/Protospace/main-website) on GitHub and click **Fork**. Create the fork under your own GitHub account.
|
||||||
|
2. Clone your fork and configure the Protospace repository as `upstream`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone git@github.com:YOUR-USERNAME/main-website.git
|
||||||
|
cd main-website/
|
||||||
|
git remote add upstream git@github.com:Protospace/main-website.git
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Create a branch for your change:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout -b update-page
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
cd public/
|
||||||
|
python3 -m http.server
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Commit your changes and push the branch to your fork:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "Update page content"
|
||||||
|
git push -u origin update-page
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Go to your fork on GitHub and click **Compare & pull request**. Set the base repository to `Protospace/main-website`, choose `master` as the base branch, describe what changed, and submit the pull request.
|
||||||
|
|
||||||
|
Maintainers will review the pull request, request changes if needed, and merge it when it is ready. After your pull request is merged, you can sync your fork before starting another change:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout master
|
||||||
|
git fetch upstream
|
||||||
|
git reset --hard upstream/master
|
||||||
|
git push origin master --force-with-lease
|
||||||
|
```
|
||||||
|
|
||||||
|
There is no build step. Edit the generated HTML and CSS files directly. Keep the existing responsive layout, local asset paths, accessibility text, metadata, and external Wiki/Portal links intact.
|
||||||
|
|
||||||
|
## Deploying changes
|
||||||
|
|
||||||
|
The production server will automatically pull changes from this repo every 5 minutes.
|
||||||
|
|
||||||
|
## Using a coding agent
|
||||||
|
|
||||||
|
A coding agent can be helpful for larger content, layout, accessibility, and consistency changes. Give it a focused task and ask it to inspect the existing files before editing. Follow the repository instructions in `AGENTS.md` when present, and review the resulting diff before opening a pull request.
|
||||||
|
Before Width: | Height: | Size: 253 KiB |
|
Before Width: | Height: | Size: 164 KiB |
|
Before Width: | Height: | Size: 265 KiB |
|
Before Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 249 KiB |
|
Before Width: | Height: | Size: 167 KiB |
|
Before Width: | Height: | Size: 289 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 308 KiB |
|
Before Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 231 KiB |
|
Before Width: | Height: | Size: 256 KiB |
|
Before Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 307 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 230 KiB |
|
Before Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 194 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 275 KiB |
@@ -1,119 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
||||||
<link rel="icon" href="/assets/favicon.png" type="image/png" />
|
|
||||||
<meta name="description" content="Explore Protospace’s electronics lab in Calgary, with workbenches, test equipment, soldering tools, and components for member projects." />
|
|
||||||
<link rel="canonical" href="https://protospace.ca/electronics-lab/" />
|
|
||||||
<title>Electronics Lab – Protospace</title>
|
|
||||||
<link rel="stylesheet" href="/styles.css" />
|
|
||||||
<script type="application/ld+json">
|
|
||||||
{
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": [
|
|
||||||
"LocalBusiness",
|
|
||||||
"NonprofitOrganization"
|
|
||||||
],
|
|
||||||
"@id": "https://protospace.ca/#organization",
|
|
||||||
"name": "Protospace",
|
|
||||||
"description": "Protospace is a volunteer-run, non-profit makerspace in Calgary with shared woodworking, metalworking, sewing, electronics, laser cutting, and 3D printing facilities.",
|
|
||||||
"url": "https://protospace.ca/",
|
|
||||||
"logo": "https://protospace.ca/assets/logo.png",
|
|
||||||
"image": "https://protospace.ca/assets/FrontProto-1024x455.jpg",
|
|
||||||
"telephone": "+1-587-774-5672",
|
|
||||||
"email": "info@protospace.ca",
|
|
||||||
"address": {
|
|
||||||
"@type": "PostalAddress",
|
|
||||||
"streetAddress": "1530 27th Avenue NE, Bay 108",
|
|
||||||
"addressLocality": "Calgary",
|
|
||||||
"addressRegion": "AB",
|
|
||||||
"postalCode": "T2E 7S6",
|
|
||||||
"addressCountry": "CA"
|
|
||||||
},
|
|
||||||
"areaServed": "Calgary, Alberta, Canada",
|
|
||||||
"priceRange": "$55/month",
|
|
||||||
"sameAs": [
|
|
||||||
"https://www.facebook.com/CalgaryProtospace/",
|
|
||||||
"https://www.youtube.com/user/calgaryprotospace/playlists"
|
|
||||||
],
|
|
||||||
"openingHoursSpecification": {
|
|
||||||
"@type": "OpeningHoursSpecification",
|
|
||||||
"dayOfWeek": "https://schema.org/Tuesday",
|
|
||||||
"opens": "19:00",
|
|
||||||
"closes": "21:00",
|
|
||||||
"description": "Public open house every Tuesday from 7:00 pm to 9:00 pm, including holidays. Visitors can drop in for a tour."
|
|
||||||
},
|
|
||||||
"contactPoint": {
|
|
||||||
"@type": "ContactPoint",
|
|
||||||
"telephone": "+1-587-774-5672",
|
|
||||||
"email": "info@protospace.ca",
|
|
||||||
"contactType": "customer service"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<div class="nav-wrap">
|
|
||||||
<a class="brand" href="/"><img src="/assets/logo.png" alt="Protospace" /></a>
|
|
||||||
<nav class="nav">
|
|
||||||
<a href="/the-space/">About</a>
|
|
||||||
<a href="/join-us/">Membership</a>
|
|
||||||
<a href="http://wiki.protospace.ca">Wiki</a>
|
|
||||||
<a href="http://my.protospace.ca">Portal</a>
|
|
||||||
<a href="/contact/">Contact</a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div class="content">
|
|
||||||
<div class="layout">
|
|
||||||
<main>
|
|
||||||
<h1>Electronics Lab</h1>
|
|
||||||
<div class="entry">
|
|
||||||
<figure>
|
|
||||||
<img width="1280" height="960" src="/assets/electronics-lab-2.jpg" alt="Electronics lab equipment" />
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
<figure>
|
|
||||||
<img width="1024" height="768" src="/assets/electronics-lab-3-1024x768.jpg" alt="Electronics workbench and equipment" />
|
|
||||||
</figure>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<aside class="sidebar">
|
|
||||||
<section>
|
|
||||||
<h2>2026 Schedule:</h2>
|
|
||||||
<p class="green">
|
|
||||||
Open House every Tuesday from 7-9 pm (even holidays). Just drop in for a tour!
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<a href="https://youtu.be/B9w5G6Rn99A"><b>Click here for a mini tour video</b></a>
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<p>1530 27th Avenue NE<br />Bay 108<br />Calgary, Alberta, Canada<br />T2E 7S6</p>
|
|
||||||
<p><b>Parking:</b> For open house, park in the front lot (southeast) or on the street.</p>
|
|
||||||
<iframe
|
|
||||||
class="map"
|
|
||||||
src="https://maps.google.com/maps?width=300&height=100&hl=en&q=Protospace%2C%20Calgary&ie=UTF8&t=&z=12&iwloc=B&output=embed"
|
|
||||||
title="Map to Protospace"
|
|
||||||
></iframe>
|
|
||||||
</section>
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<footer>
|
|
||||||
<div class="footer-inner">
|
|
||||||
<div class="footer-links">
|
|
||||||
<div><a href="https://www.youtube.com/user/calgaryprotospace/playlists">YouTube</a></div>
|
|
||||||
<div><a href="http://wiki.protospace.ca/Faq">F.A.Q.</a></div>
|
|
||||||
<div><a href="https://www.facebook.com/CalgaryProtospace/">Facebook</a></div>
|
|
||||||
</div>
|
|
||||||
<h4>PROTOSPACE 2026 · All rights reserved</h4>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -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())
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
||||||
<link rel="icon" href="/assets/favicon.png" type="image/png" />
|
|
||||||
<meta name="description" content="Explore Protospace’s laser cutting area in Calgary, including laser cutters and desktop CNC equipment for member projects." />
|
|
||||||
<link rel="canonical" href="https://protospace.ca/laser-cutters/" />
|
|
||||||
<title>Laser Cutters – Protospace</title>
|
|
||||||
<link rel="stylesheet" href="/styles.css" />
|
|
||||||
<script type="application/ld+json">
|
|
||||||
{
|
|
||||||
"@context": "https://schema.org",
|
|
||||||
"@type": [
|
|
||||||
"LocalBusiness",
|
|
||||||
"NonprofitOrganization"
|
|
||||||
],
|
|
||||||
"@id": "https://protospace.ca/#organization",
|
|
||||||
"name": "Protospace",
|
|
||||||
"description": "Protospace is a volunteer-run, non-profit makerspace in Calgary with shared woodworking, metalworking, sewing, electronics, laser cutting, and 3D printing facilities.",
|
|
||||||
"url": "https://protospace.ca/",
|
|
||||||
"logo": "https://protospace.ca/assets/logo.png",
|
|
||||||
"image": "https://protospace.ca/assets/FrontProto-1024x455.jpg",
|
|
||||||
"telephone": "+1-587-774-5672",
|
|
||||||
"email": "info@protospace.ca",
|
|
||||||
"address": {
|
|
||||||
"@type": "PostalAddress",
|
|
||||||
"streetAddress": "1530 27th Avenue NE, Bay 108",
|
|
||||||
"addressLocality": "Calgary",
|
|
||||||
"addressRegion": "AB",
|
|
||||||
"postalCode": "T2E 7S6",
|
|
||||||
"addressCountry": "CA"
|
|
||||||
},
|
|
||||||
"areaServed": "Calgary, Alberta, Canada",
|
|
||||||
"priceRange": "$55/month",
|
|
||||||
"sameAs": [
|
|
||||||
"https://www.facebook.com/CalgaryProtospace/",
|
|
||||||
"https://www.youtube.com/user/calgaryprotospace/playlists"
|
|
||||||
],
|
|
||||||
"openingHoursSpecification": {
|
|
||||||
"@type": "OpeningHoursSpecification",
|
|
||||||
"dayOfWeek": "https://schema.org/Tuesday",
|
|
||||||
"opens": "19:00",
|
|
||||||
"closes": "21:00",
|
|
||||||
"description": "Public open house every Tuesday from 7:00 pm to 9:00 pm, including holidays. Visitors can drop in for a tour."
|
|
||||||
},
|
|
||||||
"contactPoint": {
|
|
||||||
"@type": "ContactPoint",
|
|
||||||
"telephone": "+1-587-774-5672",
|
|
||||||
"email": "info@protospace.ca",
|
|
||||||
"contactType": "customer service"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<div class="nav-wrap">
|
|
||||||
<a class="brand" href="/"><img src="/assets/logo.png" alt="Protospace" /></a>
|
|
||||||
<nav class="nav">
|
|
||||||
<a href="/the-space/">About</a>
|
|
||||||
<a href="/join-us/">Membership</a>
|
|
||||||
<a href="http://wiki.protospace.ca">Wiki</a>
|
|
||||||
<a href="http://my.protospace.ca">Portal</a>
|
|
||||||
<a href="/contact/">Contact</a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<div class="content">
|
|
||||||
<div class="layout">
|
|
||||||
<main>
|
|
||||||
<h1>Laser Cutters</h1>
|
|
||||||
<div class="entry">
|
|
||||||
<figure>
|
|
||||||
<img width="1280" height="960" src="/assets/laser-cutters-1.jpg" alt="Laser cutter in the laser cutting area" />
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
<p>Our laser cutting area also includes two mini desktop CNC mills:</p>
|
|
||||||
|
|
||||||
<figure>
|
|
||||||
<img width="1280" height="960" src="/assets/laser-cutters-3.jpg" alt="Laser cutter and CNC equipment" />
|
|
||||||
</figure>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<aside class="sidebar">
|
|
||||||
<section>
|
|
||||||
<h2>2026 Schedule:</h2>
|
|
||||||
<p class="green">
|
|
||||||
Open House every Tuesday from 7-9 pm (even holidays). Just drop in for a tour!
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<a href="https://youtu.be/B9w5G6Rn99A"><b>Click here for a mini tour video</b></a>
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<p>1530 27th Avenue NE<br />Bay 108<br />Calgary, Alberta, Canada<br />T2E 7S6</p>
|
|
||||||
<p><b>Parking:</b> For open house, park in the front lot (southeast) or on the street.</p>
|
|
||||||
<iframe
|
|
||||||
class="map"
|
|
||||||
src="https://maps.google.com/maps?width=300&height=100&hl=en&q=Protospace%2C%20Calgary&ie=UTF8&t=&z=12&iwloc=B&output=embed"
|
|
||||||
title="Map to Protospace"
|
|
||||||
></iframe>
|
|
||||||
</section>
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<footer>
|
|
||||||
<div class="footer-inner">
|
|
||||||
<div class="footer-links">
|
|
||||||
<div><a href="https://www.youtube.com/user/calgaryprotospace/playlists">YouTube</a></div>
|
|
||||||
<div><a href="http://wiki.protospace.ca/Faq">F.A.Q.</a></div>
|
|
||||||
<div><a href="https://www.facebook.com/CalgaryProtospace/">Facebook</a></div>
|
|
||||||
</div>
|
|
||||||
<h4>PROTOSPACE 2026 · All rights reserved</h4>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -7,7 +7,15 @@
|
|||||||
<link rel="icon" href="/assets/favicon.png" type="image/png" />
|
<link rel="icon" href="/assets/favicon.png" type="image/png" />
|
||||||
<meta name="description" content="Explore Protospace’s 3D printer village in Calgary, with filament and resin printers available to trained members." />
|
<meta name="description" content="Explore Protospace’s 3D printer village in Calgary, with filament and resin printers available to trained members." />
|
||||||
<link rel="canonical" href="https://protospace.ca/3d-printers/" />
|
<link rel="canonical" href="https://protospace.ca/3d-printers/" />
|
||||||
|
<meta property="og:title" content="3D Printers – Protospace" />
|
||||||
|
<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:url" content="https://protospace.ca/3d-printers/" />
|
||||||
|
<meta property="og:image" content="https://protospace.ca/assets/3d-printers-1-800.jpg" />
|
||||||
|
<meta property="og:site_name" content="Protospace" />
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<title>3D Printers – Protospace</title>
|
<title>3D Printers – Protospace</title>
|
||||||
|
<link rel="preload" href="/styles.css" as="style" />
|
||||||
<link rel="stylesheet" href="/styles.css" />
|
<link rel="stylesheet" href="/styles.css" />
|
||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
{
|
{
|
||||||
@@ -58,13 +66,14 @@
|
|||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<div class="nav-wrap">
|
<div class="nav-wrap">
|
||||||
<a class="brand" href="/"><img src="/assets/logo.png" 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="http://wiki.protospace.ca">Wiki</a>
|
|
||||||
<a href="http://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>
|
||||||
@@ -74,18 +83,25 @@
|
|||||||
<h1>3D Printers</h1>
|
<h1>3D Printers</h1>
|
||||||
<div class="entry">
|
<div class="entry">
|
||||||
<figure>
|
<figure>
|
||||||
<img width="1024" height="768" src="/assets/3d-printers-1-1024x768.jpg" alt="3D printer in the printer village" />
|
<picture>
|
||||||
|
<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="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>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<figure>
|
<figure>
|
||||||
<img width="1024" height="768" src="/assets/3d-printers-2-1024x768.jpg" alt="3D printer equipment" />
|
<picture>
|
||||||
|
<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="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>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<figure>
|
<figure>
|
||||||
<img width="1024" height="768" src="/assets/3d-printers-3-1024x768.jpg" alt="3D printer in operation" />
|
<picture>
|
||||||
|
<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="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>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<p></p>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
@@ -95,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"><b>Click here for a mini tour video</b></a>
|
<a href="https://youtu.be/B9w5G6Rn99A" class="external">Watch the Protospace mini tour video</a>
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
@@ -103,6 +119,7 @@
|
|||||||
<p><b>Parking:</b> For open house, park in the front lot (southeast) or on the street.</p>
|
<p><b>Parking:</b> For open house, park in the front lot (southeast) or on the street.</p>
|
||||||
<iframe
|
<iframe
|
||||||
class="map"
|
class="map"
|
||||||
|
loading="lazy"
|
||||||
src="https://maps.google.com/maps?width=300&height=100&hl=en&q=Protospace%2C%20Calgary&ie=UTF8&t=&z=12&iwloc=B&output=embed"
|
src="https://maps.google.com/maps?width=300&height=100&hl=en&q=Protospace%2C%20Calgary&ie=UTF8&t=&z=12&iwloc=B&output=embed"
|
||||||
title="Map to Protospace"
|
title="Map to Protospace"
|
||||||
></iframe>
|
></iframe>
|
||||||
@@ -113,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="http://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>
|
||||||
<h4>PROTOSPACE 2026 · All rights reserved</h4>
|
<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 |
|
After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 252 KiB After Width: | Height: | Size: 252 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 510 KiB |
|
After Width: | Height: | Size: 296 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 452 KiB |
|
After Width: | Height: | Size: 256 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 529 KiB |
|
After Width: | Height: | Size: 313 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 90 KiB |