.mdeck (2/3): Whop static build with member and dashboard views, plus zip build script (#131)
Co-authored-by: Time <time@users.noreply.teahub.xyz>
This commit was merged in pull request #131.
This commit is contained in:
1 parent
602a511972
commit
1fb961a12a
5 files changed
+183
No files matched your search
@@ -0,0 +1 @@
|
||||
dist/
|
||||
@@ -58,6 +58,21 @@ python3 -c 'import base64,sys,zlib; c=zlib.compressobj(9,zlib.DEFLATED,-15); d=c
|
||||
|
||||
Without compression: `"https://mdecks.yolopreneur.lol/#md=" + encodeURIComponent(text)`.
|
||||
|
||||
## On Whop (static build)
|
||||
|
||||
`sh tools/whop-build.sh` writes `dist/mdeck-whop.zip` and `dist/mdeck-whop.zip.sha256` (sha256sum format, next to the zip): a static site of the editor and presenter, without the public page's pricing and sign-up sections. `sh tools/whop-check.sh` builds it, checks the checksum, unzips it, serves it like a static host and requests the view paths. Nothing is uploaded; creating the app on Whop and uploading the zip is a separate step.
|
||||
|
||||
Views to register on Whop, with the path Whop fills the ID into:
|
||||
|
||||
| View type | Path | What it shows |
|
||||
|---|---|---|
|
||||
| Experience (member view) | `/experiences/[experienceId]` | the editor and presenter |
|
||||
| Dashboard (creator's view) | `/dashboard/[companyId]` | the same editor, with **Copy prompt for your agent** |
|
||||
|
||||
No other view is declared (there is no `openapi` view yet).
|
||||
|
||||
A static host serves a dynamic segment only from a real directory or a rewrite rule. The zip ships `experiences/index.html`, `dashboard/index.html` and a `404.html`, all the same app with absolute asset paths, so `/experiences/<anything>` and `/dashboard/<anything>`, at any depth, load it; the app reads its path to tell the two views apart. This is the safe approach for any static host, chosen without a Whop build reference (docs.whop.com wasn't consulted).
|
||||
|
||||
## Honest limits
|
||||
|
||||
- Nothing is stored — close the tab without copying a link or the HTML and your text is gone.
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/bin/sh
|
||||
# Builds the Whop upload for .mdeck: one zip of a static site, and its sha256 next to it.
|
||||
#
|
||||
# ventures/mdecks/tools/whop-build.sh [outdir] (outdir defaults to ventures/mdecks/dist)
|
||||
#
|
||||
# Writes <outdir>/mdeck-whop.zip and <outdir>/mdeck-whop.zip.sha256 (sha256sum format). Nothing is uploaded.
|
||||
#
|
||||
# The site is the editor and presenter only (no pricing or marketing sections), served at:
|
||||
# /experiences/<experienceId> member view
|
||||
# /dashboard/<companyId> creator's dashboard view
|
||||
# A static host resolves a dynamic segment only with a real directory or a rewrite, so the zip ships
|
||||
# experiences/index.html, dashboard/index.html and a 404.html that are all the same app, with absolute asset
|
||||
# paths: any /experiences/<anything> or /dashboard/<anything> (however deep) lands on the app.
|
||||
set -eu
|
||||
|
||||
here=$(cd "$(dirname "$0")/.." && pwd)
|
||||
out=${1:-$here/dist}
|
||||
mkdir -p "$out"
|
||||
out=$(cd "$out" && pwd)
|
||||
zip="$out/mdeck-whop.zip"
|
||||
|
||||
HERE="$here" ZIP="$zip" python3 - <<'PY'
|
||||
import os, re, zipfile
|
||||
|
||||
here, target = os.environ["HERE"], os.environ["ZIP"]
|
||||
|
||||
def read(name):
|
||||
with open(os.path.join(here, name), encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
def sub1(pattern, repl, text, flags=0):
|
||||
new, n = re.subn(pattern, repl, text, count=1, flags=flags)
|
||||
if n != 1:
|
||||
raise SystemExit("whop-build: index.html no longer matches " + repr(pattern))
|
||||
return new
|
||||
|
||||
# The app page: index.html without the header's early-access note and button, and without everything after the
|
||||
# editor (how it works, pricing, limits, footer): those belong to the public page.
|
||||
page = read("index.html")
|
||||
page = sub1(r'\s*<p class="early-note">.*?</p>', "", page)
|
||||
page = sub1(r'\s*<a class="cta" href="https://teahub\.xyz[^>]*>Get early access</a>', "", page)
|
||||
page = sub1(r'(</main>).*?(<script src="app\.js"></script>)', r"\1\n\n\2", page, re.S)
|
||||
# Absolute paths, so the same file works at any depth; whop.js tells the two views apart.
|
||||
page = sub1(r'<script src="app\.js"></script>',
|
||||
'<script src="/app.js"></script>', page)
|
||||
page = sub1(r"</title>", "</title>\n<script src=\"/whop.js\"></script>", page)
|
||||
|
||||
app = read("app.js")
|
||||
if 'fetch("SKILL.md")' not in app:
|
||||
raise SystemExit("whop-build: app.js no longer fetches SKILL.md")
|
||||
app = app.replace('fetch("SKILL.md")', 'fetch("/SKILL.md")')
|
||||
|
||||
files = {
|
||||
"index.html": page,
|
||||
"experiences/index.html": page,
|
||||
"dashboard/index.html": page,
|
||||
"404.html": page,
|
||||
"app.js": app,
|
||||
"whop.js": read("whop.js"),
|
||||
"SKILL.md": read("SKILL.md"),
|
||||
}
|
||||
|
||||
# Same bytes in, same zip out: fixed timestamps, sorted names.
|
||||
with zipfile.ZipFile(target, "w", zipfile.ZIP_DEFLATED) as z:
|
||||
for name in sorted(files):
|
||||
info = zipfile.ZipInfo(name, (2020, 1, 1, 0, 0, 0))
|
||||
info.compress_type = zipfile.ZIP_DEFLATED
|
||||
info.external_attr = 0o644 << 16
|
||||
z.writestr(info, files[name].encode("utf-8"))
|
||||
PY
|
||||
|
||||
(cd "$out" && sha256sum mdeck-whop.zip > mdeck-whop.zip.sha256)
|
||||
echo "built $zip"
|
||||
cat "$out/mdeck-whop.zip.sha256"
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/bin/sh
|
||||
# Checks the Whop build: builds it, verifies the checksum, unzips it, serves it the way a static host does
|
||||
# (real files and a directory's index.html first, 404.html for anything else) and requests the view paths.
|
||||
#
|
||||
# sh ventures/mdecks/tools/whop-check.sh
|
||||
set -eu
|
||||
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
work=$(mktemp -d)
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
sh "$here/whop-build.sh" "$work/out"
|
||||
(cd "$work/out" && sha256sum -c mdeck-whop.zip.sha256)
|
||||
python3 -c 'import sys, zipfile; zipfile.ZipFile(sys.argv[1]).extractall(sys.argv[2])' "$work/out/mdeck-whop.zip" "$work/site"
|
||||
|
||||
SITE="$work/site" python3 - <<'PY'
|
||||
import http.server, os, threading, urllib.request, urllib.error
|
||||
|
||||
site = os.environ["SITE"]
|
||||
|
||||
class Static(http.server.SimpleHTTPRequestHandler):
|
||||
"""A static host: a real file, or a directory's index.html; anything else gets 404.html with a 404."""
|
||||
def __init__(self, *a, **k):
|
||||
super().__init__(*a, directory=site, **k)
|
||||
def do_GET(self):
|
||||
bare = self.path.split("?")[0]
|
||||
path = self.translate_path(bare)
|
||||
if os.path.isdir(path) and not bare.endswith("/"):
|
||||
return super().do_GET() # redirect to the slash form
|
||||
if os.path.isdir(path):
|
||||
path = os.path.join(path, "index.html")
|
||||
if os.path.isfile(path):
|
||||
return super().do_GET()
|
||||
with open(os.path.join(site, "404.html"), "rb") as f:
|
||||
body = f.read()
|
||||
self.send_response(404)
|
||||
self.send_header("Content-Type", "text/html")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
def log_message(self, *a):
|
||||
pass
|
||||
|
||||
srv = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Static)
|
||||
threading.Thread(target=srv.serve_forever, daemon=True).start()
|
||||
base = "http://127.0.0.1:%d" % srv.server_port
|
||||
|
||||
def get(path):
|
||||
try:
|
||||
r = urllib.request.urlopen(base + path)
|
||||
return r.status, r.read().decode()
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, e.read().decode()
|
||||
|
||||
failed = 0
|
||||
for path in ["/experiences/abc", "/experiences/abc/", "/experiences/a/b/c", "/dashboard/xyz", "/dashboard/xyz/",
|
||||
"/experiences/", "/dashboard/", "/experiences", "/dashboard"]:
|
||||
status, body = get(path)
|
||||
ok = 'id="src"' in body and 'src="/app.js"' in body and 'src="/whop.js"' in body
|
||||
print("%-22s %s serves the app: %s" % (path, status, "yes" if ok else "NO"))
|
||||
failed += not ok
|
||||
|
||||
for path, needle in [("/app.js", 'fetch("/SKILL.md")'), ("/whop.js", "data-view"), ("/SKILL.md", "mdeck")]:
|
||||
status, body = get(path)
|
||||
ok = status == 200 and needle in body
|
||||
print("%-22s %s asset ok: %s" % (path, status, "yes" if ok else "NO"))
|
||||
failed += not ok
|
||||
|
||||
status, body = get("/experiences/abc")
|
||||
for gone in ("Get early access", "Opens a public GitHub issue", "Pricing", "How it works"):
|
||||
if gone in body:
|
||||
print("public-page content left in the app: " + gone)
|
||||
failed += 1
|
||||
if 'id="agent"' not in body:
|
||||
print("agent button missing")
|
||||
failed += 1
|
||||
|
||||
raise SystemExit(1 if failed else 0)
|
||||
PY
|
||||
echo "whop build ok"
|
||||
@@ -0,0 +1,13 @@
|
||||
/* Whop build only: which of the two human views this address is. The static build serves the same app at every
|
||||
/experiences/<id> and /dashboard/<id> path, so the app reads the path to know where it is.
|
||||
/dashboard/<companyId> the creator's view: the editor with "Copy prompt for your agent"
|
||||
/experiences/<experienceId> the member's view: the editor and presenter, no agent prompt
|
||||
Anything else (the 404.html fallback on another path) is shown as the member view. */
|
||||
(function () {
|
||||
"use strict";
|
||||
var view = /^\/dashboard(\/|$)/.test(location.pathname) ? "dashboard" : "member";
|
||||
document.documentElement.setAttribute("data-view", view);
|
||||
var style = document.createElement("style");
|
||||
style.textContent = 'html[data-view="member"] #agent{display:none}';
|
||||
document.head.appendChild(style);
|
||||
})();
|
||||
Reference in new issue
Block a user