diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..70d3ce8 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/favicon.png b/public/favicon.png deleted file mode 100644 index 156231d..0000000 Binary files a/public/favicon.png and /dev/null differ diff --git a/public/icon-192.png b/public/icon-192.png new file mode 100644 index 0000000..7639ac3 Binary files /dev/null and b/public/icon-192.png differ diff --git a/public/icon-512.png b/public/icon-512.png new file mode 100644 index 0000000..064292b Binary files /dev/null and b/public/icon-512.png differ diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 0000000..98bd2ba --- /dev/null +++ b/public/manifest.webmanifest @@ -0,0 +1,6 @@ +{ + "icons": [ + {"src": "/icon-192.png", "type": "image/png", "sizes": "192x192"}, + {"src": "/icon-512.png", "type": "image/png", "sizes": "512x512"} + ] +} \ No newline at end of file diff --git a/scripts/puppeteer.ts b/scripts/puppeteer.ts index 3b17437..2fa7418 100644 --- a/scripts/puppeteer.ts +++ b/scripts/puppeteer.ts @@ -44,8 +44,12 @@ async function handleUrl(browser: Browser, url: string) { const pathname = new URL(url).pathname; console.time(pathname); - const path1 = await snap(browser, url, "base"); - const path2 = await snap(browser, url.replace(base, compare), "compare"); + const path1 = await snap(browser, `${url}?snap`, "base"); + const path2 = await snap( + browser, + `${url.replace(base, compare)}?snap`, + "compare", + ); const img1 = readScreenshot(path1); const img2 = readScreenshot(path2); const meta1 = await img1.metadata(); diff --git a/src/components/Shadowbox.astro b/src/components/Shadowbox.astro index 3831fbd..88090d9 100644 --- a/src/components/Shadowbox.astro +++ b/src/components/Shadowbox.astro @@ -2,12 +2,14 @@ interface Props { padding?: number; class?: string | undefined; + id?: string | undefined; } -const { padding = 1, class: className } = Astro.props; +const { padding = 1, class: className, id } = Astro.props; ---
{ sha && ( - + {sha.slice(0, 7)} ) diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index fcc797a..0937467 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -29,7 +29,7 @@ const { const canonicalUrl = new URL(Astro.url.pathname, Astro.site); --- - + - + + @@ -82,6 +83,7 @@ const canonicalUrl = new URL(Astro.url.pathname, Astro.site); async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js"></script> + <script src="../scripts/snap.ts"></script> <script src="../scripts/slideshow.ts"></script> <script src="../scripts/hotkeys.ts"></script> diff --git a/src/lib/__snapshots__/getPosts.spec-snapshot.ts.snap b/src/lib/__snapshots__/getPosts.spec-snapshot.ts.snap index 546014c..4e6625e 100644 --- a/src/lib/__snapshots__/getPosts.spec-snapshot.ts.snap +++ b/src/lib/__snapshots__/getPosts.spec-snapshot.ts.snap @@ -4862,393 +4862,6 @@ Yay incentives! exports[`body > post astroblog hash 2cb564f0bd8c34a3eba61786b84c0b7b 1`] = ` " -<p> - <img - class="aligncenter" - alt="Elaborate machine generating word bubbles" - title="Another look behind the curtain. In this case it's yet more elaborate machinery." - width="450px" - src="https://user-images.githubusercontent.com/46881/266202247-5bcb4d04-f5db-4c4e-bff3-a92f589e3d29.png" - > -</p> -<blockquote> - <p> - <em> - This is another X-Treme Nerd Interlude post. -Last time we announced, mercifully briefly, - <a - href="https://blog.beeminder.com/blogmorphosis" - title="Ditching WordPress and a Shiny Blog Redesign" - > - our shiny new blog redesign - </a> - and if you’re a normal human you should read that, nod thoughtfully, say “looks lovely”, and be on your merry way. -The rest of you can frolic deep in the weeds here with - <a - href="https://www.nathanarthur.com/" - title="Nathan's personal homepage" - > - Nathan Arthur - </a> - aka narthur — the mastermind and architect of everything you’re looking at. -Also it’s a nice primer on what the heck a Static Site Generator (SSG) is, in case you were curious about that. - </em> - </p> -</blockquote> -<p> - WordPress is the de facto tool for building blogs and, as Danny said last time, it served Beeminder well for many years. -But now we’re using a static site generator called - <a - href="https://astro.build" - title="You can't go wrong with a tool whose website has a TLD of build, right?" - > - Astro - </a> - . -Let’s talk about why and how! -</p> -<p> - We’ll start with the downsides of WordPress. -WordPress is a server-rendered content management system. -That means that whenever you visit a page on a WordPress website, WordPress retrieves that page’s data from a database, renders it to HTML, and sends it to you. -</p> -<p> - <img - src="https://user-images.githubusercontent.com/4655422/266149193-f7f05af8-aaea-4fa9-98dc-9884e01b7b06.png" - alt="Dynamic website architecture" - title="Browser requests page -> Server gathers data -> Server builds page -> Server returns page" - > -</p> -<p> - This architecture is powerful — it allows websites to tailor each page for the current user. -And in WordPress’s case it allows non-technical admins to build and customize their sites, no coding required. -</p> -<h3> - The Idea -</h3> -<p> - There’s an alternative — the static site. -A static site is simply a set of HTML, CSS, and JavaScript files, served as-is, without waiting for the server to painstakingly construct them before sending them to you. -</p> -<p> - <img - src="https://user-images.githubusercontent.com/4655422/266149192-a882668a-2fdb-4bed-8ad5-4d76832fa948.png" - alt="Static website architecture" - title="Browser requests page -> Server returns static page" - > -</p> -<p> - It would be impractical to build most websites this way. -Hand coding HTML files for every post on the Beeminder blog would be painful. -And then a change to the footer, say, would require editing every HTML file again. -</p> -<p> - But what if we did that WordPress-style page construction all at once, one time? -Then we’d have a set of static files. -We call that one-time painstaking construction the build step. -</p> -<p> - <img - src="https://user-images.githubusercontent.com/4655422/266149190-2f985f24-1b03-436f-a683-160589d98333.png" - alt="Static website architecture with builds" - title="Static build is triggered -> blah blah like WordPress but for every single page on the website all at once. And then from then on it's the same as the diagram above for a static site." - > -</p> -<p> - Whenever you add a new blog post, or change anything in an old blog post, or change anything at all about the website, you trigger a build. -The Static Site Generator does its WordPressing, as we’ll call it, to every page and blog post on the whole site again. -But, crucially, it’s not doing this in real time with eager blog readers waiting for it. -It regenerates the static site and when the new files are ready, it swaps the old ones out and the new ones in. -End users are only ever asking for and getting served static pages. -</p> -<p> - Basically we have the best of both worlds: let the build process do all the HTML generating, putting the same footer on every page and all that, but let end users get served simple static pages. -It’s also more secure. -WordPress has a whole admin interface to let you add posts and modify the site — a static site needs none of that. -And it’s drastically cheaper (or even free, depending on how much traffic it gets) to host a static site. -And content delivery networks (CDNs) can cache those files closer to end users which adds to the speed advantage. -</p> -<p> - The Beeminder blog is particularly well-situated to take advantage of all of those benefits. -Its content is primarily static. -The non-static bits (like the comments) can use third-party embeds. -Beeminder’s staff are technical and not code-shy (so no need for everything to be WYSIWYG). -And Beeminder had been spending a lot on blog hosting. -</p> -<p> - <em> - (Danny adds: Does this mean the migration paid for itself? -Haha, no. -We initially thought it might — we were paying around $1k/year for WordPress hosting — but then decided we didn’t care. -We really like this new system! -And it might eventually pay off even just money-wise, who knows?) - </em> -</p> -<h3> - Objectives & Requirements -</h3> -<p> - Alright, enough why, let’s run through the what and how! -We had some specific boxes that we wanted the new blog to tick. -First, we needed to preserve what the Beeminder team liked about their workflow. -That meant pulling in external markdown source files for each post, and being agnostic about where Beeminder may decide to store that markdown. -And builds needed to be fast enough that fixing a typo wouldn’t be excruciating. -</p> -<p> - We also wanted the blog to be nice and maintainable — easy for future developers to improve the blog. -And of course not increasing Beeminder’s security exposure or other costs. -Like I said, security and cost should both be improved a lot by a static site generator. -</p> -<p> - In terms of what users see when read the blog, we needed to - <a - href="https://blog.beeminder.com/pareto" - title="Short version is that A Pareto-dominates B if A beats or ties B on every dimension you might care about" - > - Pareto-dominate - </a> - the old blog. -So of course all the old posts should stay the same, the RSS feed shouldn’t break. -Beeminder also has a bunch of fancy custom markdown augmentations like footnotes and LaTeX-style references that needed to keep functioning. -</p> -<p> - And as icing on the cake, we wanted the blog to feel nicer for end users — faster and more modern and more in line with Beeminder’s visual identity. -</p> -<h3> - Choosing a Static Site Generator -</h3> -<p> - We needed to take our backlog of content and use a static site generator to convert that content into static HTML, CSS, and JavaScript files. -</p> -<p> - <a - href="https://jamstack.org/generators/" - title="Handy list of SSGs, at least as of 2023" - > - Many options exist - </a> - . -I didn’t want to use those that I’d used previously. - <a - href="https://jekyllrb.com/" - title="Very popular, Ruby-based" - > - Jekyll - </a> - — not my preferred stack. - <a - href="https://www.gatsbyjs.com/" - title="People say it's great?" - > - Gatsby - </a> - — too unintuitive. - <a - href="https://nextjs.org/" - title="Involves React" - > - Next.js - </a> - — too many unneeded features. -Also Brent Yorgey recommended - <a - href="https://jaspervdj.be/hakyll" - title="Like Jekyll but with Haskell" - > - Hakyll - </a> - which seems cool but he recommended it too late and it would have been hard since I’m bad at Haskell. -</p> -<p> - One I hadn’t used previously was - <a - href="https://astro.build/" - title="You can't go wrong with a tool whose website has a TLD of build, right?" - > - Astro - </a> - . -I watched - <a - href="https://www.youtube.com/watch?v=3TVy6GdtNuQ" - title="Svelte Crash Course" - > - a video - </a> - of someone building a simple site in Astro, and I was impressed. -It seemed like our use case was exactly Astro’s happy path, which made me feel much more comfortable that I could spend more time executing and less time fighting with the framework. -I also appreciated that I could use my preferred stack — node, pnpm, typescript, vitest. -I liked how the default Astro templating language is JSX-like, and how it co-locates server JS, client JS, HTML, and CSS in each component file. -I also really liked that it comes with folder structure routing out of the box. -And I liked how easy it makes using external data in static generation, just using the standard fetch API. -</p> -<p> - Astro had everything we wanted. -It has file-based routing with dynamic route segments and simple route-based data fetching. -It’s written in JavaScript and supports TypeScript out of the box. -It uses file-based components. -The Astro templating format is close enough to JSX to make a React developer feel at home, while sidestepping all the complexity that comes from using React. -It’s built on Vite, making for good build performance. -And it has an RSS plugin which made setting up our feed quite simple. -</p> -<p> - Additionally Astro proved beneficial in several other ways that we didn’t think to look for. -It has built-in link prefetching. -It includes experimental support for image optimization (still working on setting that up!) and static redirects. - -And it has a hot-reloading local dev server. -</p> -<h3> - Choosing a Host -</h3> -<p> - Danny suggested Render.com. -I hadn’t used it before, but I’m glad we did. -It was a great experience. -I’ve never enjoyed deploying to AWS or Google Cloud. -In contrast, Render.com integrates with GitHub, providing painless git-triggered deployments. -Also blueprints. -</p> -<p> - Initially I planned to have GitHub Actions run our builds. -I planned to take the custom PHP code from Beeminder’s WordPress plugin and run it as a stand-alone PHP proxy in front of Etherpad inside the build action. -That way I could avoid reimplementing the custom functionality from the old blog for things like footnotes. -The Continuous Integration (CI) action would then deploy the built blog to whatever static host we settled on. -</p> -<p> - However Render.com has its own Netlify-style static site builds, so no need for GitHub Actions. -Instead I reimplemented the custom logic in TypeScript. -While this meant more effort in reproducing the features from the old blog, it kept the deploy pipeline much simpler and kept everything in the main TypeScript codebase, which I think was the right call for long-term maintainability. -</p> -<h3> - Migrating the Data -</h3> -<p> - Beeminder hosts the raw markdown behind the blog posts in an Etherpad instance separate from WordPress, so we didn’t need to worry about exporting post content from WordPress. -But we still needed to export post metadata, including the Etherpad source URLs, as well as some info about the authors, and then use that data in the builds for old posts that aren’t using the new markdown frontmatter. -</p> -<p> - I used two WordPress plugins to export the data — one for posts and one for users. -I then took this data, imported it into Google Sheets, and exported just the columns we needed into CSV files which are stored in the blog repository. -</p> -<p> - At build time the code uses these CSV files to look up information about posts that were written before the blog rewrite. -For new posts, this meta information will be stored in frontmatter at the top of the markdown files. -</p> -<h3> - Build Optimization -</h3> -<p> - I spent too much time optimizing build performance. -It’s a fun puzzle. -</p> -<p> - I - <a - href="https://en.wikipedia.org/wiki/Memoization" - title="Not to be confused with Memorization, points out Wikipedia, helpfully" - > - memoized - </a> - liberally. -You don’t want to repeat any expensive computations. -In addition I cached the raw markdown we requested from Etherpad. -In a production build, this is cached in-memory. -Locally (i.e., in development) it’s cached in the filesystem to avoid needing to wait for a bunch of requests to finish before seeing the result of a test build. -</p> -<p> - There are some fancy markdown features that I needed to use a virtual DOM library to implement. -I started by using - <a - href="https://www.npmjs.com/package/jsdom" - title="Excerpt: jsdom is a pure-JavaScript implementation of many web standards, notably the WHATWG DOM and HTML Standards, for use with Node.js" - > - jsdom - </a> - , -but this was very slow. -I ended up switching to - <a - href="https://www.npmjs.com/package/happy-dom" - title="Excerpt: Happy DOM is a JavaScript implementation of a web browser without its graphical user interface. It includes many web standards from WHATWG DOM and HTML. The goal of Happy DOM is to emulate enough of a web browser to be useful for testing, scraping web sites and server-side rendering. Happy DOM focuses heavily on performance and can be used as an alternative to JSDOM." - > - happy-dom - </a> - and it was much faster. -</p> -<p> - When I first started fetching markdown from Etherpad, I made all the requests at once. -This flooded Beeminder’s Etherpad instance and caused our builds to fail. -I switched to doing these requests synchronously, which allowed the builds to complete, but made builds take a long time. -Finally I switched to using - <a - href="https://www.npmjs.com/package/p-limit" - title="Excerpt: Run multiple promise-returning & async functions with limited concurrency" - > - p-limit - </a> - to send the requests as fast as possible while limiting the number of simultaneously-active requests. -I tested that limit, timing different values, until I found one that was optimal for build speed and reliability. -</p> -<p> - <br> -   - <br> -</p> -<p> - In conclusion… voila? -Goodbye WordPress, hello Astro. -And it’s being served by Render.com at zero monthly cost. -Here’s the full final stack in case you got bored partway through the above and are jumping here for the punchline: -</p> -<ul> - <li> - Pnpm - package manager - </li> - <li> - Zod - data schemas and transforms - </li> - <li> - Astro - static site generator - </li> - <li> - Vitest - testing framework - </li> - <li> - Happy-dom - virtual dom - </li> - <li> - Render.com - hosting and builds - </li> - <li> - GitHub - version control and ci - </li> - <li> - TypeScript - static types - </li> - <li> - Node - build runtime - </li> - <li> - Vite - Astro’s underlying build tool - </li> -</ul> -<p> - Danny would also like me to remind everyone that there’s a $10 honey money bounty for any bugs or typos you may find on the blog. -You can report those in the comments or - <a - href="https://forum.beeminder.com/t/shiny-new-blog-redesign/11107?u=dreev" - title="Shiny new blog redesign" - > - in the forum - </a> - ! -</p> -" -`; - -exports[`body > post astroblog hash f214b52e8804417aa13a661edf7e99c0 1`] = ` -" <p> <img class=\\"aligncenter\\" @@ -68399,7 +68012,7 @@ But don’t listen to me, see what a true " `; -exports[`body > post predict hash 1c0bdd2bcebb1e1c91531bb0bce392d0 1`] = ` +exports[`body > post predict hash b9ed963e091616cadde1b4326a1ba560 1`] = ` " <p> <img @@ -68814,421 +68427,6 @@ See also " `; -exports[`body > post predict hash b9ed963e091616cadde1b4326a1ba560 1`] = ` -" -<p> - <img - class="aligncenter" - alt="Bee riding the Manifold.markets logo" - title="Wheeeee. It's a bird, and a bee. But not like that, geez." - width="450px" - src="https://blog.beeminder.com/wp-content/uploads/2022/05/faire-manifold-bee-logo.jpeg" - > -</p> -<p> - A fun fact about predicting your own behavior, particularly publicly, is that the act of predicting it changes the prediction. -“I’m 75% likely to maintain my Duolingo streak all year, but now that I’ve said so I’m actually 90% likely, but now that I’ve said - <em> - that - </em> - , …” -Or what happens when the probability starts very low but you add a wager? -It’s like this self-describing xkcd: -</p> -<p> - <img - alt="xkcd #688, Self-Description" - title="The contents of any one panel are dependent on the contents of every panel including itself. The graph of panel dependencies is complete and bidirectional, and each node has a loop. The mouseover text has two hundred and forty-two characters." - src="https://imgs.xkcd.com/comics/self_description.png" - > -</p> -<p> - Or like the sentence, “This sentence consists of exactly fifty-seven characters.” - <a - class="footnote" - id="CODE1" - href="#CODE" - > - [1] - </a> -</p> -<p> - (This, incidentally, is part of what’s hard about predicting whether superhuman AI will accidentally destroy everything humans value in the universe. -If we actually had consensus on the probability being high, we’d be galvanized to change course. -It’s like if you knew you had a high chance of accidentally taking a fatal dose of medicine, you’d double check the dosage and make the probability low again. -Not that I’m arguing against sounding alarm bells.) -</p> -<p> - Anyway, a fun fact about Beeminder, from way back in the - <a - href="https://blog.beeminder.com/beenamer" - title="The story of Beeminder's past and current name" - > - Kibotzer - </a> - days, is that it started as essentially a prediction market. -</p> -<p> - You can hear - <a - href="https://vimeo.com/groups/17842/videos/14980876" - title="New York 2010" - > - Bethany tell the story - </a> - in an ancient Quantified Self talk from when Beeminder was a side project. -Back then, instead of automatically paying Beeminder when you went off track, Beeminder just showed you a graph of your progress and your commitment. -The commitment contracts themselves were arranged separately. -The inaugural commitment contracts were Bethany and me staking $2,000 each on a gallery of personal fitness and productivity goals during the summer of 2010: -</p> -<ul> - <li> - Steps - </li> - <li> - Weight - </li> - <li> - Work hours on our day jobs - </li> - <li> - Time spent with our kids (still babies back then! and, weirdly, this was a do-less goal for me) - </li> - <li> - Time spent building our fun new self-tracking commitment device app - </li> - <li> - Pushups - </li> - <li> - Desserts - </li> - <li> - Reading fiction - </li> -</ul> -<p> - So what did “staking $2,000” mean? -We were each promising to pay some friend or family member that $2k if we didn’t stay on track on all of those goals. -Which friend or family member was determined by auction, coordinated manually by email at first. -We started the bidding at $10, it quickly went to $50, and then we sold them for $65 each the next day. -</p> -<p> - That meant that our friends were paying $65 for the chance to win $2k. -Which implies market odds of 96.8% that we’d succeed at our goals that summer. -Which we did, phew. -Obviously the probability would not have been nearly so high without the money at stake. -</p> -<p> - Emboldened by that trial, we set up a slightly more formal auction interface for other people to use: -</p> -<p> - <img - class="aligncenter" - alt="Screenshot of the auction results for Melanie's weight loss commitment contract" - title="Screenshot of the auction results for Melanie's weight loss commitment contract" - src="https://user-images.githubusercontent.com/9928/268165487-c0b52d3f-7b0e-4ca4-91c2-a8b9d8732fbd.png" - > -</p> -<p> - That one turns the game theory up to eleven. -The commitment contract there was my sister Melanie’s weight loss goal, with $600 of her money at stake. -The bidders specify in the first row of that table the most they’d pay to be the beneficiary of the commitment contract. -The auction mechanism then does a bunch of math - <a - class="footnote" - id="MATH1" - href="#MATH" - > - [2] - </a> - to decide how much people actually pay (second row) and to divvy the potential booty (third row). -As it turned out Laurie, our mother, got half the contract — half of Melanie’s $600 should she have failed at her goal. - <a - href="https://ece.illinois.edu/alumni/awards/yaaa/19-Yang" - title="David's also a Beeminder advisor and helped us a ton in the early days" - > - David Yang - </a> - got 25%, and - <a - href="https://en.wikipedia.org/wiki/Daniel_Goldstein" - title="Obviously Dan is a big deal. Our kids were particularly impressed when we were watching a popular TV show called Brain Games and Dan showed up as one of the experts" - > - Dan Goldstein - </a> - got the remaining 25%. - <a - href="http://www.davidreiley.com/" - title="David is our close friend and has featured on the blog before, in particular in a post on Unintended Consequences" - > - David Reiley - </a> - , - <a - href="https://bsoule.com/" - title="Everyone else is getting a link so Bee's getting one too this time!" - > - Bethany - </a> - , and I didn’t bid enough to get anything. -And then, once again, - <a - href="https://melzafit.com" - title="Melanie was Beeminder's original Resident Fitness Expert and now has her own personal fitness business which you should check out!" - > - Melanie - </a> - did in fact stay on track so those shares didn’t end up paying out. -Each person’s bid implies the odds they assign to Melanie actually staying on track on her weight goal. -The bottom right number — 2.5% — is the aggregated market odds. -</p> -<p> - Today, Beeminder only offers infinitely bad odds. -You pay for going off track, but don’t get paid for staying on track. -You could think of that as perfectly fair odds if using Beeminder meant a 100% chance of always staying on track. -It doesn’t but these days we view this all very differently, largely because staying on vs going off track is not actually a binary outcome. -You’ll inevitably go off track - <em> - sometimes - </em> - and that’s fine — you’re really just paying for the motivation Beeminder is providing the rest of the time. -We have a whole series of blog posts about this: - <a - href="https://blog.beeminder.com/defail" - title="AKA Beeminder Revenue Proportional To User Awesomeness" - > - Derailing Is Not Failing - </a> - , - <a - href="https://blog.beeminder.com/depunish" - title="This one's more controversial -- many users really like to treat payments to Beeminder as punishment, which makes sense, but this is a helpful framing for some" - > - Paying Is Not Punishment - </a> - , and - <a - href="https://blog.beeminder.com/nailingit" - title="A positive spin on Derailing Is Not Failing" - > - Derailing It Is Nailing It - </a> - . -</p> -<p> - Probably if you’re reading to the end of Beeminder blog posts you’re already on board with all that. - <a - class="footnote" - id="DEFAIL1" - href="#DEFAIL" - > - [3] - </a> - But if you’re a prediction market person, we’d actually love to convince you to try predicting your own behavior via - <a - href="https://manifold.markets" - title="They just call themselves Manifold now but markets (as in prediction markets) are the main thing you'll find there" - > - Manifold markets - </a> - . -(Disclosure: I’m an investor in Manifold.) -Here are some examples of people (including us) doing so: -</p> -<ul> - <li> - <a - href="https://en.wikipedia.org/wiki/Danny_O%27Brien_(journalist)" - title="Fun fact: he coined the term 'life hack' in 2004" - > - Danny O’Brien - </a> - (also a Beeminder fan!) -wants to - <a - href="https://manifold.markets/DannyOBrien/will-i-danny-obrien-reach-my-target" - title="Will I, Danny O'Brien, reach my target weight of 185 lbs by December 1, 2023?" - > - lose weight - </a> - and - <a - href="https://manifold.markets/DannyOBrien/will-i-danny-obrien-launch-a-public" - title="I guess he prefers to call it a journal. Wikipedia does call him a journalist." - > - launch a blog - </a> - </li> - <li> - I wanted - <a - href="https://manifold.markets/dreev/will-beeminders-next-blog-post-be-a" - title="Will Beeminder's next blog post be about prediction markets as commitment devices?" - > - to publish this blog post months ago - </a> - (sheepish-emoji) - </li> - <li> - Bethany wanted to - <a - href="https://manifold.markets/bethanysoule/will-bee-deploy-code-that-uses-stri" - title="Will Bee deploy code that uses Stripe's PaymentIntents API by Sunday?" - > - get an infrastructure upgrade deployed - </a> - </li> - <li> - (See myriad other examples by browsing markets tagged - <a - href="https://manifold.markets/questions?topic=commitment-devices&f=all&s=random" - title="There was also a meta market on whether 50 markets would end up with this tag, which they did" - > - Commitment Devices - </a> - or - <a - href="https://manifold.markets/questions?topic=personal-goals&f=all&s=random" - title="Some of these might be embarrassing or NSFW but they're all public, so" - > - Personal Goals - </a> - ) - </li> -</ul> -<p> - Last but not least, I want to - <a - href="https://manifold.markets/dreev/will-it-be-possible-to-transfer-man" - title="Will it be possible to transfer mana to honey money and vice versa at Beeminder's booth at Manifest?" - > - launch a new feature - </a> - to allow Manifold users to transfer mana (that’s Manifold’s play-money currency) to honey money (Beeminder’s currency, aka store credit on Beeminder) and vice versa in time to talk about it and use it at - <a - href="https://www.manifestconference.net/" - title="Manifold's inaugural forecasting conference" - > - Manifest - </a> - ! -</p> -<p> - <br> -   - <br> -</p> -<h2> - Footnotes -</h2> -<p> - <a - class="footnote" - id="CODE" - href="#CODE1" - > - [1] - </a> - If you’re wondering how I made that sentence come out right, I just let Mathematica brute-force it: -</p> -<pre> - <code> - For[i = 2, i < 1000, i++, - s = "This sentence consists of exactly "<> - IntegerName[i]<>" characters."; - If[StringLength[s] == i, Print[i, ": ", s]]] - </code> -</pre> -<p> - I then thought to let GPT-4’s Code Interpreter - <a - href="https://chat.openai.com/share/89181be9-4a7a-4926-a9f8-555c65448f8e" - title="It's writing and executing Python code on the fly to answer purely natural-language questions." - > - have a shot at it - </a> - . -It gets there, with minimal coaxing. -As of 2023, this feels utterly gobsmacking. -</p> -<p> - <a - class="footnote" - id="MATH" - href="#MATH1" - > - [2] - </a> - If you really want to know, we have an - <a - href="https://doc.beeminder.com/bidder" - title="The First Auctioned-off Beeminder Commitment Contract" - > - ancient document - </a> - about it. -If I had to justify it, it started with the idea to let everyone buy in at any amount of money they chose. -Call that the pot. -Then you divvy up shares of the contract -(the money you win if the person auctioning off the commitment contract doesn’t do what they committed to) -by just giving each person a fraction of the contract equal to the fraction they contributed to the pot. -The more you put in, the more of the payout you get (if the contract pays out). -It’s like a parimutuel market. -</p> -<p> - But then! -This is where the wild game theory comes in. -Deciding how much to pay into the pot is a tricky strategic problem that depends pretty delicately on what other people are paying in. -</p> -<p> - Say two other bidders have put in $100 each on a $1000 contract. -And let’s say the person creating this commitment contract is doomed, as in, you’re 100% certain they’ll be paying that $1000. -So those other bidders are currently getting $500 each by spending $100. -If you put in $100 as well then the three of you will get $333 each. -This is still profitable and you want to put more into the pot, but at about $222 you start losing money. -Each person putting $222.22 into the pot turns out to be the Nash equilibrium of this game. -</p> -<p> - So the idea of this mechanism is to compute those equilibrium bids for everyone. -You just say how much you think the whole contract is worth, and the mechanism plays the Nash equilibrium for you, under the maybe-false assumption that everyone has said their true value for the whole contract. -Bidders don’t have to understand any of that. -They can just keep bumping up their bids as long as the mechanism is giving them a price and a fraction of the contract that feels fair. -</p> -<p> - It’s all preposterous amounts of overkill/overcomplication but it did seem to work. -</p> -<p> - <a - class="footnote" - id="DEFAIL" - href="#DEFAIL1" - > - [3] - </a> - To recap the argument, if you view Beeminder’s stings as punishment, that can -(a) encourage an excuse-making mindset over a results-oriented mindset, and maybe more importantly, -(b) encourage you to make your goals less ambitious. -If you accept that some derailments are inevitable — part of the cost of the service, just nudges or rumble strips keeping you on track — you can dial in a sweet spot where you’re being pushed to do as much as possible (or whatever maximizes the motivational value you get from Beeminder) at minimal cost. -See also - <a - href="https://ymeskhout.substack.com/" - title="Yassine is funny and cool. Probably read his Substack." - > - Yassine Meskhout - </a> - ’s notion of - <a - href="https://forum.beeminder.com/t/yassine-meskhouts-taxing-vs-punitive-commitment-contracts/9262" - title="Apparently he's never published this but he gave us permission to quote him in the Beeminder forum" - > - taxing vs punitive commitment contracts - </a> - . -</p> -" -`; - exports[`body > post preeat hash 8e7d083d3033a47cebeb9b41336dcae1 1`] = ` " <p> @@ -72988,7 +72186,7 @@ See especially the case of Pact, whose scheme of paying successful users we̵ " `; -exports[`body > post readwise hash cfc7dd19133c9cf22f76789324c48ebf 1`] = ` +exports[`body > post readwise hash df9779ab68a4458e6036baf279071d00 1`] = ` " <p> <img @@ -73078,96 +72276,6 @@ Just work through your items, either deleting them, archiving them, or otherwise " `; -exports[`body > post readwise hash df9779ab68a4458e6036baf279071d00 1`] = ` -" -<p> - <img - class="aligncenter" - width="450px" - alt="A robotic bee in a sea of books" - title="Readwise and Beeminder sittin' in a tree, I-N-T-E-G-R-A-T-I-N-G" - src="https://user-images.githubusercontent.com/46881/271141213-610ee865-d1c3-49d4-aab0-611d8df01109.png" - > -</p> -<p> - <a - href="https://readwise.io/read" - title="Note: Readwise and Readwise Reader are distinct things" - > - Readwise Reader - </a> - is a powerful tool for “power readers”. -It’s like a supercharged read-it-later app, with first-class support for notes and highlights and tags. -Now, you can keep track of your Readwise Reader items using Beeminder. -</p> -<p> - You save things like web pages, PDFs, YouTube videos, Twitter threads, or aim it at an RSS feed or an email newsletter. -It collects them all and makes them easy to read on their own. -Web pages show just the juicy content you’re looking for. -YouTube videos are shown alongside their transcript, for example, and the transcript highlights as the words are spoken in the video. -You can click on a line in the transcript to jump right to it. -Of course, it wouldn’t be a good read-it-later app if it didn’t save your progress in the video (or anything else you use Readwise Reader for). -</p> -<p> - As awesome as Readwise Reader is, it’s no surprise that some folks go into a “read-it-later” frenzy. -The save rate tends to be higher than the read rate, and that pile accumulates… -I’d show you some good quotes about this problem, but they’re somewhere in my later pile. -We recently got a note from the Readwise Reader folks that they added some API endpoints for fetching items that are already in your account. -Readwise Reader has been sitting near the top of the candidate autodata integration list for a while — and folks have loved our - <a - href="https://blog.beeminder.com/pocket" - title="Beeminder blog post announcing our Pocket integration" - > - Pocket integration - </a> - for a while, and Adam’s Readwise Reader had gotten a little overwhelming… -</p> -<h2> - How does it work? -</h2> -<p> - You set a limit for a Readwise Reader location, like Inbox, Later, or Shortlist. -If you’re already under the limit, great! -You’ll have to stay under the limit each day or pay a penalty. -If you’re currently over that limit, Beeminder will set up a goal to get you down to that limit, and then stay under it. -</p> -<p> - If you want to adjust how long it’ll take to get to your limit, you can do that once the goal is created. -</p> -<h2> - Wait, I’m a Readwise Reader user new to Beeminder! -</h2> -<p> - Welcome aboard! -You might want to check out our - <a - href="https://help.beeminder.com/category/5-quick-start-overview" - title="A collection of 15 newbee-oriented articles starting witih "What is Beeminder?"" - > - getting started guide - </a> - in our Help Docs, and then get yourself signed up. -Assuming you’re on board with the commitment device, paying-money-if-you-go-off-track bit, the beauty of an autodata integration is that you don’t normally need to interact with Beeminder once you’ve gone through the process above of setting up your commitment. -Just work through your items, either deleting them, archiving them, or otherwise moving them out of the location (your Inbox or Later or Shortlist) when Beeminder yells at you and you’re good. -</p> -<p> - Ready to dive in? -</p> -<center> - <a href="https://www.beeminder.com/readwisereader"> - <span style="background-color:#1d76db;color:#FFF;padding:0 16px;font-size:16px;font-weight:600;line-height:3;border-radius:4px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;display:inline-block !important"> - Beemind your read-it-latering with Readwise Reader! - </span> - </a> -</center> -<p> - <br> -   - <br> -</p> -" -`; - exports[`body > post recommit hash 092241b965df40e4027dee893e4621e3 1`] = ` " <p> diff --git a/src/pages/[slug].astro b/src/pages/[slug].astro index f0ece14..715200f 100644 --- a/src/pages/[slug].astro +++ b/src/pages/[slug].astro @@ -1,5 +1,5 @@ --- -import type { InferGetStaticPropsType } from "astro"; +import type { InferGetStaticPropsType } from "astro"; import Tags from "../components/Tags.astro"; import getPosts from "../lib/getPosts"; import Shadowbox from "../components/Shadowbox.astro"; @@ -12,10 +12,10 @@ import Paginated from "../layouts/Paginated.astro"; export async function getStaticPaths() { const posts = await getPosts({ includeUnpublished: true, - sort: true + sort: true, }); - const published = (p: Post) => p.status === "publish" + const published = (p: Post) => p.status === "publish"; return posts.map((post: Post, i: number) => ({ params: { @@ -31,22 +31,9 @@ export async function getStaticPaths() { type Props = InferGetStaticPropsType<typeof getStaticPaths>; -const { - post, - newer, - older -} = Astro.props as Props; +const { post, newer, older } = Astro.props as Props; -const { - content, - title, - tags, - disqus_id, - excerpt, - image, - status, - slug, -} = post +const { content, title, tags, disqus_id, excerpt, image, status, slug } = post; const isDraft = status === "draft"; const className = isDraft ? "draft" : undefined; @@ -66,32 +53,39 @@ const { extracted, ...imageProps } = image || {}; url: `/${older.slug}`, }} > -<div class="post"> - <Shadowbox padding={2} class={className}> - <PostMeta - post={{ - ...post, - title, - }} - includeExcerpt={false} - linkTitle={false} - Heading="h1" - /> - {extracted ? undefined : <img class="aligncenter" {...imageProps} />} - <Typography set:html={content} /> - <Tags tags={tags} /> - </Shadowbox> + <div class="post"> + <Shadowbox padding={2} class={className}> + <PostMeta + post={{ + ...post, + title, + }} + includeExcerpt={false} + linkTitle={false} + Heading="h1" + /> + {extracted ? undefined : <img class="aligncenter" {...imageProps} />} + <Typography set:html={content} /> + <Tags tags={tags} /> + </Shadowbox> - <Shadowbox padding={2}> - {isDraft ? <p>Comments are disabled on DRAFT pages.</p> : <Comments id={disqus_id} url={`https://blog.beeminder.com/${slug}/`} />} - </Shadowbox> -</div> + <Shadowbox id="comments" padding={2}> + { + isDraft ? ( + <p>Comments are disabled on DRAFT pages.</p> + ) : ( + <Comments + id={disqus_id} + url={`https://blog.beeminder.com/${slug}/`} + /> + ) + } + </Shadowbox> + </div> </Paginated> - <style> .post :global(.draft) { border: 1rem solid #ff700a; } </style> - diff --git a/src/scripts/snap.ts b/src/scripts/snap.ts new file mode 100644 index 0000000..2d1e5c3 --- /dev/null +++ b/src/scripts/snap.ts @@ -0,0 +1,8 @@ +// Purpose: Improve reliability of puppeteer diffing + +const urlParams = new URLSearchParams(window.location.search); + +if (urlParams.has("snap")) { + const body = document.querySelector("body"); + body?.classList.add("snap"); +} diff --git a/src/styles/global.css b/src/styles/global.css index 0ef04e6..2e1a2a2 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -177,3 +177,14 @@ ol { margin-left: auto; margin-right: auto; } + +/* .snap purpose: improve puppeteer diffing reliability */ + +body.snap #comments, +body.snap #sha { + display: none; +} + +body.snap img { + filter: brightness(0); +}