See the full architecture guide first.
- Project Structure
- Tools
- Scripts
- Client Storage
- Demo Mode
- DevTools Helpers
- Design System
- DOM Anchors
- Test Strategy
- Deploy
We use Svelte as the UI framework and Vite as the builder.
main/: app entry point and global things like design tokens.colors.css: color tokens.fonts.css: used fonts.reset.css: CSS reset to minimize the difference between browsers.index.css: global styles.index.ts: JS entry point.environment.ts: how client core should work with browser environment.database.ts: choose the SQLite storage engine and open the database.db-worker.ts: SQLite worker with the drivers for both storage engines. See client storage.browser.ts: connect core stores to global browser settings likedocument.title.extension.ts: make network request through the browser extension.
benchmark/: UI performance benchmark, which is loaded only by?benchmarkin URL. See benchmark guide.demo/: the page which copies the files of the demo database and the script to build it.pages/: Svelte components for pages.ui/: shared components between different pages. Some people call it “UI kit”.public/: static files like favicon and manifests.stores/: web client’s own smart stores. For instance, router on top of URL using Nano Stores Router.lib/: browser helpers without stores or components.postcss/: PostCSS plugins to check CSS and optimize it. Check the plugin’s descriptions for more information.vite/: build helpers, which landings use too.stories/: visual tests for pages and UI components by Storybook. The main way to test web client.scripts/: scripts to check for popular errors, optimize files after Vite build, and deploy. Check the script’s descriptions for further details.dist/:pnpm buildwill build the result here for deployment..storybook/: visual tests tool config.app.html: builder entry point of the app.nginx.conf: web server config for Docker image. Server’sentrypoint.tsapply changes to it..browserslistrc: browsers, which we support. See actual browsers list..size-limit.json: budget for JS bundles, whole app page, and landing size. Don’t be afraid to tune the limit. We put it so tight that it makes you feel a small pain every time you add a significant amount of code.global.d.ts: missed web platform types and fixes for Vite imports.
- Storybook to check styles for popular mistakes and keep CSS code style formatting.
- Size Limit to prevent accidental bloat of JS bundle size, for instance, by adding a big dependency.
- Browserslist is the single config with target browsers for team and all tools.
- KeyUX implements keyboard hotkeys and focus management around
aria-attributes.
pnpm -F web test: run all web client tests.pnpm -F web visual: run visual test server.pnpm -F web chromatic: publish visual tests and generate diffs for changed stories.pnpm -F web build: build production files inweb/dist/.pnpm -F web benchmark: run UI performance benchmark.pnpm -F web build-demo: load the feeds fromweb/demo/demo-feeds.opml&web/demo/demo-sources.jsonand write demo DB dump.
We keep SQLite as a file in OPFS, but only cross-origin isolated pages can do it. Browsers without the isolation, like Safari, use the opfs-sahpool VFS, which works without multi-tab support.
The engine is chosen on the first start and saved as slowreader:vfs in localStorage, since the engines can not read each other’s files.
I mode to quickly see the app with some example content. We pre-generate database to start it quickly.
/copy-demo-db copies the demo database to OPFS and the demo settings to localStorage, then opens the app.
The page writes a plain file from a worker, since Safari has no createWritable() on pages. The opfs-sahpool engine can not see the file, so in that mode the app imports the file into the pool on the next start and removes it.
Demo Database workflow rebuilds the files every Monday and pushes them with landing screenshots and OG image by demo/Dockerfile as slowreader-demo image. Run pnpm update-docker to pin the latest one in the main image.
Debug helpers from core/devtools.ts are exported in window.slowreader and can be called from the browser console:
-
slowreader.moveLastSyncedToPast(days): move refresh markers of all feedsdaysback, so the next refresh will load old posts. Use it to test refresh right after OPML import:await slowreader.moveLastSyncedToPast(30) await slowreader.refreshPosts()
-
slowreader.enablePostDebug(): print post data and its source on opening post popup. -
slowreader.enableProxyDebug(): print proxy request and response headers.
All colors must be declared in main/colors.css.
Fonts are declared in main/fonts.css.
For icons, we use Material Design Icons and <Icon> component.
<script lang="ts">
import { mdiAccount } from '@mdi/js'
</script>
<Icon path={mdiAccount} />data-anchor marks elements for external code: benchmark scenarios, e2e tests, and user plugins (scripts and user styles).
<Button anchor="read-page">{$t.readPage}</Button>
<div data-anchor="popup"></div>document.querySelectorAll('[data-anchor="post"]')Since clients don’t have much logic (we moved logic to the client core), we don’t need to use unit tests.
We can use only visual tests to test web clients UI. We are using Storybook and Chromatic snapshots.
Since we use a free plan, we run Chromatic on CI only daily (or on pull request with run-visual label). Daily builds use TurboSnap to snapshot only stories affected by changes.
We deploy the latest Storybook of main branch to staging: dev.slowreader.app/ui/
You can check Storybook of pull request by adding /ui/ to the preview deploy URL: like https://preview-100.slowreader.hplush.dev/ui/
But those visuals can be very complex. We do not just test buttons in different states. We test whole pages by mocking network requests and stores states. We use small JS in stories to test animations or some JS code.
You can use <Scene> to change core stores and mock HTTP.
We should cover with stories every page and every UI component which can be used by other developers. Storybook could be used to find UI component and by i18n team to check the context of the message. But you can avoid stories for low-level components used only by single page.
Web client runs in the single image as nginx with static files. nginx passes other requests to the server.
Both preview and staging have Storybook at /ui/ route.
To return app HTML on app’s routes, we export RegExp of all routes to routes.regexp and use it in nginx.conf and in Vite.
After the build, we save hashes of all JS files and main HTML pages to dist/manifest.json and add import map with JS hashes to HTML. Server’s entrypoint.ts puts these hashes to Content Security Policy, so browsers run only JS from our build.