Riven Stock
Riven Stock: Windows 앱으로 리벤 재고와 warframe.market 판매를 관리
화장지 종류와 가격을 입력해 가성비 좋은 제품을 골라줍니다
Pick the Charmin, Quilted Northern, Angel Soft or Kirkland Signature packs on the shelf, type in the prices, and see which one costs less per sheet or per square foot.
커머스JavaScript · CSSGitHub @ahatzz11온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Toilet Paper Math: which roll is actually the better deal
README 전체를 불러오는 중입니다.
README 전문 보기 ↗A small site at https://toiletpaper.app for comparing Charmin, Quilted Northern, Angel Soft and Kirkland Signature (Costco) toilet paper, whose roll names (Mega, Mega XL, Super Mega and the rest) don't say how much paper you get.
| Route | Page |
|---|---|
/ | Deal calculator: pick the packs on the shelf, type the prices, see which costs less, or which is the softest or the best balance |
/convert | Roll converter: what one roll equals in every other roll, with a chart of all rolls |
/softness | The softness scale: every rated line on a 0 to 10 scale, one long strip of paper, least soft first. A few reference points that aren't products sit on it too: sandpaper at 0, port-a-potty paper at 2, a cloud at 11 just past the end. They live only on this page. Plain HTML and CSS, no JS |
/about | How it works, for people: why sheets beat "rolls" in three cards, then a short FAQ (<details> accordion, works without JS) |
/data | Data & API: every roll in one table, the sources table, the API with a live example, the link format, and links to llms.txt, llms-full.txt and openapi.json. It sits under How it works: no tab of its own, the About tab shows as current, and the breadcrumb is Home > About > Data & API |
/charmin-mega-vs-quilted-northern-mega, /how-many-sheets-in-a-mega-roll, /cheapest-toilet-paper-per-sheet | Short guides that answer one question each and link into the tools |
/api/products, /api/compare | Read-only JSON API, described in /openapi.json |
/robots.txt, /sitemap.xml, /llms.txt, /llms-full.txt | For crawlers and AI assistants |
Both tools compare by sheet count by default. Paper area and area × ply are one click away.
The nav has four tabs in a pill (Best deal, Roll converter, Softness, How it works). Below 1024px the tabs shorten to Deal, Converter, Softness and About. On phones (up to 640px) and small tablets (641 to 699px) the tabs fill a row under the brand, so the phone header stays within 103px. /about links to /data from the "where do the numbers come from" answer and from a "See the data & API" link at the end of the page. test/browser.test.mjs measures it at 320, 360, 375, 390, 768 and 1440px. Every page ends with the same one-line footer, "Not affiliated with any brand · Open source on GitHub", linking to https://github.com/ahatzz11/toiletpaper. It stays on one line down to 320px. The guides are linked from the /about FAQ answers and the 404 page, and listed in the sitemap and llms.txt. Old links to /about#sources and /about#data land on the matching FAQ answer, which app.js opens.
public/data.js holds every number. The rule for sources: manufacturer numbers beat retailer listings.
sources.source note.charmin.com (sheets, claim) or Hannaford (size, pack total), so the sources table shows which numbers came from whom.regularId: null, and its products get claim: null (and size: null if it has no size names). Its own roll is the unit: the converter shows what one roll equals in the other brands' regular rolls, and the tables say "no claim". Kirkland Signature is the one such brand. Its sheets, sheet size, ply, rolls and pack totals all come from costco.com, and the printed square-foot totals match our math exactly.derived. A number we couldn't find printed and borrowed from a sibling product is estimated. Both show as tags on the site.checked in data.js and UPDATED in src/site/config.mjs when you recheck.Softness has no industry standard and no printed number, so data.js has a softness block: one coarse score from 0 to 10 per product line, and each product's soft key names its line. Regular rolls and custom rolls are never rated. The method, as it appears on /softness and /data#softness:
score: null and shows as "not rated".| Line | Score | Sources |
|---|---|---|
| Charmin Ultra Soft | 10 | TechGearLab 8.6 (tied top), Good Housekeeping 2026 softest (+1), Reviewed 2024 comfort pick |
| Quilted Northern Ultra Plush | 8 | TechGearLab 7.8, Good Housekeeping 2026 "some of the highest scores for softness", Reviewed 2024 |
| Quilted Northern Ultra Soft & Strong | 7 | TechGearLab 7.4, Good Housekeeping 2026 "not quite as soft" as Ultra Plush, Reviewed 2024 |
| Charmin Ultra Strong | 6 | TechGearLab 6.8 (rounds to 7), Reviewed 2024 "not as soft as some others" (-1), Charmin sells it for strength |
| Angel Soft | 5 | TechGearLab 5.4 (tested Angel Soft Double), Reviewed 2024 |
| Charmin Essentials (1-ply) | not rated | Tests found rated the discontinued Essentials Soft |
| Kirkland Signature Bath Tissue | not rated | Only old tests (Consumer Reports 2014, a 2018 panel of an older roll) or a blog; Consumer Reports 2025 is paywalled |
| Kirkland Signature Ultra Soft | not rated | No independent test found |
Consumer Reports' 2025 toilet paper ratings and Wirecutter couldn't be read (paywall, blocked), so nothing from them is used. Recheck the sources when you update softness.checked.
The calculator's priority (p in links and the API) picks the winner:
cheap, the default): the lowest price per unit (per sheet, per sq ft or per sq ft × ply, following mode). Softness doesn't count.balanced): start from the cheapest pack. A softer pack wins if it costs at most 10% more per unit for each softness point it gains over the cheapest, so 2 points softer may cost up to 20% more. Of the packs that qualify, the softest wins, then the cheaper. If the cheapest pack isn't rated, Balanced can't weigh it and keeps the cheapest. BALANCE_STEP in core.js sets the 10%.soft): the softest rated pack wins; the cheaper one breaks a tie. Packs that aren't rated are left out and named on the result card. If no pack is rated, the cheapest wins and the card says so.The result card explains the pick in plain words, like "It costs 20% more per sheet than Pack A but is softer (7 vs 5 out of 10). Balanced pays up to 20% more for that."
There is no framework and no client build step. Every number comes from one file, and the server and the browser run the same code.
public/data.js: every brand and roll (sheet count, sheet size, ply, the "= N regular" claim, sources with URLs). Edit numbers here. Product ids appear in links and the API, so don't rename them.public/core.js: the math, the link format and the HTML for both tools. The browser loads it, and the build and the Worker run it too.public/app.js: browser wiring only: events, keeping the address bar in sync, the background rolls.public/styles.css, public/icon.svg and the icon PNGs, public/media/: static files.src/site/pages.mjs: every page, with copy, tables and FAQ built from data.js. src/site/layout.mjs wraps them in the shared head, header and footer (meta tags, canonical, JSON-LD, icons). src/site/files.mjs writes robots.txt, sitemap.xml, llms.txt, llms-full.txt, openapi.json, the web manifest, /api/products and the IndexNow key file. src/site/config.mjs holds the origin, the sitemap date (UPDATED) and the IndexNow key.src/worker.js: request handling (routing, caching, ranges, the API, server rendering of linked state).scripts/build-inline.mjs: renders all of the above and writes one ES module Worker, dist-inline/index.js, with every file inlined.To add a page, add it to src/site/pages.mjs. It gets a canonical URL, a sitemap entry and a line in llms.txt.
npm install
npm run build:inline # writes dist-inline/index.js
npm test # runs build:inline, then the tests in test/
npm run serve # serves the built Worker at http://localhost:8787
npm run media # re-renders public/media/ and the icons (only after changing the scene or icon.svg)
npm run indexnow # after a deploy: tells IndexNow every URL in the live sitemap
The calculator keeps its state in the query string and updates it as you type (history.replaceState, debounced), so the address bar is always a link to what's on screen. "Copy link" copies it.
https://toiletpaper.app/?mode=area&a=charmin-ultra-soft-mega-xl,6,9.99&b=quilted-northern-ultra-soft-strong-mega,12,13.49
mode: sheets, area or areaply (ply also works).p: cheap (default, left out of links), balanced or soft. cheapest, balance and softest also work.a to h: one pack each, <product id>,<rolls>,<price>. The price may be empty. A roll that isn't listed is custom:<sheets>:<width>x<length>:<ply>./convert?roll=<product id>&mode=area.Bad values are dropped and the page falls back to its defaults. The Worker renders linked state on the server too, so the first paint and the link preview (og:description) show the linked result. The canonical URL never includes the query.
The URL is the only saved state. Nothing goes to localStorage or cookies, so opening a link always shows that link's packs, and your own comparison lives in your history (Back) and in links you copied. The tool controls carry autocomplete="off" and app.js sets them from the parsed URL on load, so a browser restoring old form values can't override a link. test/browser.test.mjs checks this in Chrome with a returning visitor's profile; it skips itself when Chrome isn't installed.
Read-only, open CORS (Access-Control-Allow-Origin: *, OPTIONS preflight), GET and HEAD only (anything else gets a 405 JSON error).
GET /api/products: every brand and roll, cached for an hour, with an ETag. Each product has softness: { score, outOf, line, basis, sources }, or null when it isn't rated. A top-level softness object carries the method.GET /api/compare?mode=sheets&p=cheap&a=<id>,<rolls>,<price>&b=...: 2 to 8 packs. Returns the price per 100 sheets, per sq ft and per sq ft × ply and the softness score for each pack, best (the winner for p) and cheapest flags, priority and pickReason, the winner (or a tie), the percent differences, a one-line summary and a link to the same comparison in the calculator. Cached for 5 minutes. Bad input gets a 400 with a JSON error./openapi.json describes both (OpenAPI 3.1).
The Worker serves every route itself, with no bindings:
/index or /index.html gets a 301 to the clean path. Unknown paths get the 404 page (noindex, with links to the tools and guides).If-None-Match match gets a 304). HTML ETags are weak (W/"..."), because Cloudflare's edge strips strong ETags from HTML but keeps weak ones. If-None-Match matches weak and strong forms either way. If-Range needs a strong ETag, so it only works on static files./styles.<hash>.css. The hash is the first 10 hex characters of the file's SHA-256. The build rewrites every href, src and meta content that points at one of these files to the hashed path. A reference it can't rewrite fails the build.Range: bytes=a-b, a- or -n gets a 206. A range past the end gets a 416. Several ranges or a malformed header get the whole file.www.toiletpaper.app gets a 301 to the apex with the same path and query. The rule is redirectTarget in src/index.ts, which the build copies in.| What | Cache-Control |
|---|---|
| Hashed files | public, max-age=31536000, immutable |
robots.txt, sitemap.xml, llms.txt, llms-full.txt, openapi.json, site.webmanifest, the IndexNow key, /api/products | public, max-age=3600 |
/favicon.ico, /apple-touch-icon.png | public, max-age=86400 |
/api/compare | public, max-age=300 |
| HTML, the 404 page and other plain paths | no-cache (revalidated with the ETag) |
The plain paths (/styles.css) still serve the current file for old cached HTML. A hash from an older deploy also gets the current file, with no-cache.
Each page's inline <style> and the mascot's width and height keep the header small if styles.css fails to load.
The build fails if the output would reach 3 MB. Most of the size is the base64 video, so keep share.mp4 small.
theme-color, and icons (SVG, ICO, apple-touch-icon, a web manifest with 192 and 512 PNGs).WebSite on every page, WebApplication on the two tools, BreadcrumbList on subpages, and FAQPage on /about, built from the same list as the visible FAQ. A test checks that they match./data. llms-full.txt adds the full roll table, the method and the FAQ.public/media/share.png (1200x630) and an 8 second looping video, share.mp4. npm run media renders both from scripts/share-media/scene.html, which uses the site's own background rolls. Frame 0 of the video is the image. There is no twitter:player card, because it needs a separate HTTPS player page for an iframe and would replace the large image card.prefers-reduced-motion.The key is in src/site/config.mjs and served at /<key>.txt. It is public by design (search engines fetch it to confirm ownership), so secrets scans should skip it. After a deploy, npm run indexnow checks the live key file, reads the live sitemap and posts its URLs to https://api.indexnow.org/indexnow.
What is live:
toiletpaper on the Personal Cloudflare account. Its script is dist-inline/index.js, uploaded as a single ES module with every file inlined.toiletpaper.app and www.toiletpaper.app. The Worker itself sends www to the apex with a 301.To deploy a change, run npm test (it rebuilds dist-inline/index.js and checks it), then use one of these:
dist-inline/index.js as the toiletpaper Worker script (a script PUT with the file as an ES module). Only if the domains change, attach them with PUT /accounts/{account_id}/workers/domains.cf deploy --profile personal. It reads cloudflare.config.ts, and vite.config.ts regenerates dist-inline/index.js before the build. cf deploy --dry-run --profile personal builds without uploading.dist-inline/ is not committed. Build it before deploying.
Not affiliated with Charmin, Quilted Northern, Angel Soft, Kirkland Signature or Costco.