7.0 KiB
Website
One Nuxt 4 + Nuxt UI deployment made of self-contained apps. Each app is a Nuxt layer
in layers/<name>/ with its own pages, components, composables, server routes and config; layers in that folder are
picked up automatically. The root only holds what apps share: the shell (app/app.vue, global CSS, theme), an empty /, and the Baserow
client server/utils/baserow.ts (listRows, createRow, updateRow, listFields, each taking a table ID).
NUXT_BASEROW_URL and NUXT_BASEROW_TOKEN are shared; each app reads its own table IDs.
| Path | App |
|---|---|
/diary |
Diary — layers/diary/ (password protected) |
/wishlist |
Wishlist — layers/wishlist/ |
pnpm install
cp .env.example .env
pnpm dev
Adding an app
- Create
layers/<name>/withapp/pages/<name>.vue(orapp/pages/<name>/index.vue), and whatever else it needs:app/components,app/composables,server/api/<name>/…,shared/, and anuxt.config.tsfor its runtime config. - Keep it contained: serve its API under
/api/<name>/, and prefixuseState/useFetchkeys and app config with the app name. All layers share one auto-import namespace, so give components and exported utils names that won't clash with another app's (e.g.DiaryEntry, notEntry). - Don't use
~/inside a layer — it points to the rootapp/. Use relative imports.
Wishlist
A public shelf of the books in the Baserow wishlist table (NUXT_BASEROW_WISHLIST_TABLE_ID): cover, German title
and author per card. A card opens its Thalia link. Search matches title, author, ISBN and Thalia link;
sort by author (last name) or title.
Table: isbn (text), thalia (URL), cover (URL), books (link to the books table) and the lookups
title (German title) and authors.
Covers: cover is used if set. Otherwise the browser looks the ISBN up on Open Library
(openlibrary.org/isbn/<isbn>.json) and posts the cover URL to /api/wishlist/books/<id>/cover, which saves it to the
row, so each book is looked up only until it has a cover. That route is public but only accepts
https://covers.openlibrary.org/b/id/<n>-M.jpg and never overwrites a cover. Books Open Library has no cover for
get a generated tile; paste any image URL into cover to give them one.
layers/wishlist/
nuxt.config.ts baserowWishlistTableId
app/
pages/wishlist.vue search, sort, grid
components/WishlistCard.vue cover + title + author, links to Thalia
composables/
useWishlist.ts loads, searches and sorts the books
useWishlistCover.ts cover from the table or Open Library, saves it back
server/
api/wishlist/books/ GET list, POST [id]/cover
utils/wishlist.ts Baserow row -> WishlistBook
shared/types/wishlist.ts
Diary
A password-protected diary of reviews (books, movies, albums) and notes, built with Nuxt 4 + Nuxt UI on top of two self-hosted Baserow tables.
Configuration
| Variable | Purpose |
|---|---|
NUXT_BASEROW_URL |
Baserow instance, no trailing slash |
NUXT_BASEROW_TOKEN |
Database token with read and create access to both tables |
NUXT_BASEROW_REVIEWS_TABLE_ID |
Reviews table |
NUXT_BASEROW_NOTES_TABLE_ID |
Notes table |
NUXT_BASEROW_WISHLIST_TABLE_ID |
Wishlist table (see Wishlist) |
NUXT_DIARY_PASSWORD |
Password that unlocks the diary |
NUXT_SESSION_SECRET |
Signs the login cookie, at least 32 characters (openssl rand -hex 32) |
In layers/diary/app/app.config.ts, diary.showCovers: true shows a generated title tile next to each review.
What it does
/diary: reviews and notes on one timeline, newest first, grouped by year.- Only entries with text are shown. Long text is collapsed behind "Read more".
- Search matches every word against the kind (
review,note), the medium (book,movie,album), the date (aug 2025,2025-08), the title, the author and the text. Matches in the text are highlighted. - "Log" opens a form with a Note / Review switch. A review is written for a book picked from the books table;
its "Read on" date is saved as that book's
read_date. - Until the password is entered, only a password prompt is shown and every
/api/diary/*route except/api/diary/sessionand/api/diary/loginreturns 401. A correct password sets a signed, http-only cookie valid for 30 days. ChangingNUXT_SESSION_SECRETlogs out every browser.
Code layout
layers/diary/
nuxt.config.ts runtime config (env vars above)
app/
app.config.ts diary.showCovers
pages/diary.vue password gate around the timeline
components/
DiaryTimeline.vue the diary: search, Log button, entries grouped by year
DiaryEntry.vue one timeline entry (date, title, text, read more)
LogModal.vue Note / Review switch around the two forms
NoteForm.vue, ReviewForm.vue
PasswordModal.vue
CoverTile.vue generated cover, used when showCovers is on
composables/
useDiary.ts loads reviews + notes, searches, sorts and groups them by year
useSaveEntry.ts shared submit logic of the forms
useLog.ts open/close state of the Log modal
utils/ dates, icons, search highlighting, error messages
types/entry.ts a timeline entry (review or note)
server/
middleware/diary-auth.ts locks /api/diary/* until unlocked
api/diary/ session, login, reviews and notes (GET list, POST create), books (for the review form)
utils/
diaryTable.ts the diary's table IDs (the Baserow client itself is in the root server/utils)
review.ts, note.ts, book.ts Baserow row -> app type
auth.ts session cookie and password check
validation.ts readValidBody(event, zodSchema)
shared/
types/ Review, Note
utils/medium.ts the review media: label and creator label
utils/schemas.ts zod schemas used by both forms and API
Baserow tables
Reviews: books (link to the books table), review (long text), medium (single select: Book / Movie /
Album) and the lookups through books: title (title_german), author_or_director (authors) and
date (read_date). The books table is found through the books link, so it needs no table ID.
Books: title, title_german, authors (link) and read_date (date).
Notes: Datum (date) and Notes (long text).
Rows without a date are skipped. Text is plain text: line breaks are kept and it is never rendered as HTML.
Adding a medium
Reviews currently link only to books, so the form always saves Book. Other media need their own link field.
- Add the option to the
mediumsingle select in Baserow. - Add it to
Mediuminlayers/diary/shared/types/review.ts, toMEDIUMSinlayers/diary/shared/utils/medium.ts(itslabelmust match the Baserow option) and toMEDIUM_ICONSinlayers/diary/app/utils/icons.ts.