### Description
None
### Package Changes
None
### Screenshots
None
### Reference
None
### Checklist
- [x] A milestone is set
- [ ] The related issues has been linked to this branch
Reviewed-on: #314
Co-authored-by: SquidSpirit <squid@squidspirit.com>
### Description
- Add cache for downloading tool chain.
### Package Changes
None
### Screenshots
None
### Reference
None
### Checklist
- [x] A milestone is set
- [ ] The related issues has been linked to this branch
Reviewed-on: #313
Co-authored-by: SquidSpirit <squid@squidspirit.com>
### Description
None
### Package Changes
None
### Screenshots
None
### Reference
None
### Checklist
- [x] A milestone is set
- [ ] The related issues has been linked to this branch
Reviewed-on: #312
Co-authored-by: SquidSpirit <squid@squidspirit.com>
### Description
Parameterize the deployment manifests so a second instance (for example
`blog-beta`) can be generated without hand-editing `blog` to `blog-beta`.
- `Makefile` gains `NAME` (default `blog`), `FRONTEND_PORT` / `BACKEND_PORT`,
and `BACKEND_IMAGE` / `BACKEND_TAG` / `FRONTEND_IMAGE` / `FRONTEND_TAG`.
- Sources are resolved per instance: `<NAME>.secret.yaml`, `<NAME>.config.yaml`
and `<NAME>.pod.yaml` are used when present, otherwise the shared `blog.*`
template is used.
- The manifests use `__NAME__`, `__FRONTEND_PORT__`, `__BACKEND_PORT__`,
`__*_IMAGE__` and `__*_TAG__` placeholders, substituted by the Makefile.
- Image repositories are no longer affected by the name substitution; the
registry/repo and the tag are configurable independently.
- The generated bundle is now `<NAME>.bundle.yaml` (production is still
`blog.bundle.yaml`).
- `README.md` updated accordingly.
Usage examples:
```sh
make # writes blog.bundle.yaml
make NAME=blog-beta BACKEND_TAG=latest FRONTEND_TAG=latest
make NAME=blog-beta FRONTEND_PORT=10021 BACKEND_PORT=10022
```
### Package Changes
None
### Screenshots
N/A
### Reference
No related issue (NO-ISSUE).
### Checklist
- [x] A milestone is set
- [ ] The related issues has been linked to this branch
---
Generated by Zed with Deepseek Flash
Reviewed-on: #311
Co-authored-by: SquidSpirit <squid@squidspirit.com>
### Description
None
### Package Changes
None
### Screenshots
None
### Reference
None
### Checklist
- [x] A milestone is set
- [ ] The related issues has been linked to this branch
Reviewed-on: #310
Co-authored-by: SquidSpirit <squid@squidspirit.com>
### Description
- Use `GITEA_TOKEN` instead of tokens set by ourselves.
### Package Changes
None
### Screenshots
None
### Reference
None
### Checklist
- [x] A milestone is set
- [ ] The related issues has been linked to this branch
Reviewed-on: #309
Co-authored-by: SquidSpirit <squid@squidspirit.com>
### Description
None
### Package Changes
None
### Screenshots
None
### Reference
None
### Checklist
- [x] A milestone is set
- [ ] The related issues has been linked to this branch
Reviewed-on: #307
Co-authored-by: SquidSpirit <squid@squidspirit.com>
## Description
Improve the deployment workflow:
- Add Kubernetes manifests under `manifest/`: a Pod running Postgres, Qdrant, Redis, backend and frontend, with Secret/config templates, a Makefile that bundles them into a single file, and a README. The generated bundle is git-ignored.
- Add a backend CI workflow running `cargo check`, `cargo clippy` and `cargo test`.
- Rework the deployment workflow: build the frontend and backend images in parallel via a matrix, and add a job that packages `manifest/` as a release artifact.
## Package Changes
None
## Screenshots
N/A
## Reference
Resolves#235
## Checklist
- [x] A milestone is set
- [x] The related issues has been linked to this branch
---
Generated by Zed with Deepseek Flash
Reviewed-on: #305
Co-authored-by: SquidSpirit <squid@squidspirit.com>
### Description
On a physical Android device, opening the mobile search drawer could leave it with an abnormal height: once the on-screen keyboard opened, the drawer's top edge — and the focused keyword input — ended up above the top of the screen. It happened roughly half the time, and always with autofocus.
Root cause, measured on device with a `visualViewport` listener:
- Chrome on Android **pans** the visual viewport to reveal the focused input, so `visualViewport.offsetTop` grows (up to ~230px in the captures) while `window.innerHeight` and `visualViewport.height` stay unchanged.
- Panning only fires `visualViewport` **`scroll`** events, but `vaul-svelte` subscribed to `resize` only, so `onVisualViewportChange` never ran while the viewport was panned. The drawer kept the `bottom` offset computed for its previous position (`bottom = innerHeight - visualViewport.height`) and was left floating above the visible area.
- With the keyboard open, Chrome could also stretch the drawer past its natural height, which showed up as a blank gap between the keyword input and the footer buttons.
- Dismissing the keyboard did not restore the drawer, because the `isInput(activeElement) || keyboardIsOpen` guard can be false at that moment (`blur` fires before the resize event, and `keyboardIsOpen` is a delta-derived toggle).
Fix — `frontend/patches/vaul-svelte@1.0.0-next.7.patch` (applied by bun via `patchedDependencies`):
1. subscribe to `visualViewport` `scroll` in addition to `resize`, so the repositioning also runs while the viewport is panned;
2. subtract `visualViewport.offsetTop` from the drawer's `bottom`, so the drawer rests on the keyboard instead of above it;
3. cap the drawer height at its natural height and at the visible area, so keyboard repositioning can only make it shorter;
4. clear the inline `height`/`bottom` once the keyboard is gone, restoring the drawer's natural layout.
`frontend/Containerfile` additionally copies `patches/` into both install stages, since `bun install --frozen-lockfile` fails with "Couldn't find patch file" once `patchedDependencies` is declared.
Verified on a physical Android 10 device (Chrome 153): the drawer's `bottom` now tracks `(innerHeight - visualViewport.height) - visualViewport.offsetTop`, the drawer stays fully inside the visible area, and dismissing the keyboard restores it.
Upstream context: the same logic lives in React `vaul` — emilkowalski/vaul#619 ("The drawer goes outside the screen on iOS when input is clicked"). PR #633 covers part of it but is unmerged, so this stays a local patch until then.
### Package Changes
None. No package was added, removed or version-bumped. `vaul-svelte@1.0.0-next.7` is now patched locally (`patchedDependencies` → `patches/vaul-svelte@1.0.0-next.7.patch`).
### Screenshots
N/A — this is an on-screen-keyboard interaction fix, nothing is visible while the keyboard is closed. (Before/after `visualViewport` measurements are quoted in the Description.)
### Reference
Resolves#302
### Checklist
- [x] A milestone is set
- [x] The related issues has been linked to this branch
---
Generated by Zed coding agent with Deepseek Flash
Reviewed-on: #304
Co-authored-by: SquidSpirit <squid@squidspirit.com>
## Description
This PR generates sitemap.xml and feed.xml from the frontend instead of the manual/toolbox script, so
the public URL scheme lives in one place and the documents are never hand-mutated.
- New SvelteKit server routes /sitemap.xml and /feed.xml, generated on demand with a short
Cache-Control (public, max-age=300). There is no server-side cache, so the app regenerates on a
cache miss; browsers, crawlers and any intermediary honour the header.
- A shared PostPage entity and ListPostPagesUseCase map published posts to their public URLs using
resolve(), so the frontend route table stays the single source of truth.
- sitemap.xml (sitemap package) lists every published post plus the static pages only the frontend
knows about: home, post list, and the terms documents.
- feed.xml (feed package) contains only posts, capped at the 20 most recent. Static pages never
appear in the feed.
- The backend now exposes, per post, the preview image mime type and size (for a valid RSS enclosure)
and the post updated_time. The frontend uses updated_time as sitemap lastmod, while the feed pubDate
remains the published time.
## Package Changes
| Package | Action | Version |
| --- | --- | --- |
| feed | Added | 6.0.0 |
| sitemap | Added | 9.0.1 |
| sanitize-html | Updated | 2.17.3 -> 2.17.7 |
No Cargo.toml changes.
## Screenshots
N/A (XML endpoints).
## Reference
Resolves#117
## Checklist
- [x] A milestone is set
- [x] The related issues has been linked to this branch
---
Generated by Zed coding agent with Deepseek Flash
Reviewed-on: #303
Co-authored-by: SquidSpirit <squid@squidspirit.com>
### Description
Integrate Google AdSense into the blog frontend in a configuration-driven way, and add the legal disclosures AdSense requires.
- Load `adsbygoogle.js` through `GoogleAdsense.svelte`, gated by `PUBLIC_ADSENSE_CLIENT_ID` (mirrors the existing `GoogleAnalytics.svelte` pattern), and remove the hardcoded script from `app.html`.
- Emit `<meta name="google-adsense-account">` from `<svelte:head>` so site ownership stays verifiable from the server-rendered HTML even though the script itself is injected client-side.
- Disclose third-party advertising/analytics (Google AdSense & Analytics cookies, personalized-ad opt-out links, EEA/UK/Switzerland consent) in the privacy policy.
- Also includes a `NO-ISSUE` cleanup replacing obsolete Tailwind utility classes in `TitleScreenAnimatedTags.svelte`.
### Package Changes
None
### Screenshots
N/A — integration/config only, no visible UI change until Auto Ads is enabled in the AdSense dashboard.
### Reference
Resolves#200
### Checklist
- [x] A milestone is set
- [x] The related issues has been linked to this branch
---
Generated by Zed agent with Deepseek Flash
Reviewed-on: #301
Co-authored-by: SquidSpirit <squid@squidspirit.com>
## Description
Rework the search entry point so users can actually find it, instead of a floating button tucked in the bottom-right corner where it gets overlooked.
- Add a navbar search launcher: a search-bar-styled button on desktop and an icon button on mobile. Both open the existing search-and-filter dialog (desktop) / drawer (mobile), keeping the keyword and label fields.
- Remove the floating action button and its now-dead wiring: delete `FilteringButton`, drop the dialog from `PostOverallPage`, and remove the unused `LabelsListedStore` provider and SSR label load on `/post`.
- Indicate active state: a filter icon inside the desktop launcher when a label filter is set, and a dot badge on the mobile launcher when a keyword or label is active.
- Focus the keyword input when the dialog opens, and add `aria-expanded` / `aria-haspopup` to the launcher plus `aria-label` / `aria-expanded` to the navigation drawer buttons.
- Unify every scrim (dialog, drawer, navigation drawer) on a blurred `bg-black/15 backdrop-blur-md` overlay.
- Compensate the fixed navbar with the scrollbar width while a dialog locks scrolling, so the page no longer shifts when the scrollbar disappears.
- Lazy-load the search dialog so its code (dialog/drawer/command, zod) no longer ships on every page. Measured initial JS: `/` and `/post` drop from 301 kB to 239 kB gzip (-20%).
## Package Changes
_No response_
## Screenshots
<!-- TODO: add screenshots of the navbar search launcher and the search dialog -->
## Reference
Resolves#291
## Checklist
- [x] A milestone is set
- [x] The related issues has been linked to this branch
---
Generated by Zed coding agent with Deepseek Flash
Reviewed-on: #300
Co-authored-by: SquidSpirit <squid@squidspirit.com>
## Description
Add social media sharing to the post content page.
- Extract a reusable `ShareButton` component that renders an `<a>` when a share URL is provided, or a `<button>` for click-based actions, and takes the icon through a `children` snippet.
- Add share buttons on the post page: LinkedIn, Facebook, X, and Email, plus a native share button (Web Share API) that is shown on mobile only.
- Add share URL builders to `Links` (`EMAIL_SHARE`, `FACEBOOK_SHARE`, `LINKEDIN_SHARE`, `X_SHARE`) and the related labels to `Strings`.
- Move the footer icon labels into `Strings` and add a project skill documenting the frontend strings/links conventions.
## Package Changes
_No response_
## Screenshots
<!-- TODO: add screenshots of the share buttons -->
## Reference
Resolves#205
## Checklist
- [x] A milestone is set
- [x] The related issues has been linked to this branch
---
Generated by Zed coding agent with Deepseek Flash
Reviewed-on: #299
Co-authored-by: SquidSpirit <squid@squidspirit.com>
### Description
This PR introduces support for dark background colors on post labels.
Specifically, the following changes have been made:
1. **Luminance & Color Contrast**: Added `isDark` and `contrastingTextColor` getters in `ColorViewModel`. The color contrast detection has been improved to use the standard sRGB relative luminance formula (per WCAG 2.x standard) with a threshold of `0.179` to determine if a background is dark, ensuring much more accurate contrast adjustments than a simple YIQ calculation.
2. **Text Color Adjustment**: Sets the text color of the `PostLabel` dynamically to `#ffffff` if the background is dark, and inherits the default color if the background is light.
3. **Indicator Dot Styling**: Adjusts the background color of the small decorative dot inside `PostLabel` using a lighter variant (`lighten(0.4)`) on dark labels, and a darker variant (`darken(0.2)`) on light labels to ensure visual clarity.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#165
### Checklist
- [x] A milestone is set
- [x] The related issues has been linked to this branch
Reviewed-on: #298
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
This PR configures and registers MCP (Model Context Protocol) servers to enhance AI-assisted development workflow, corrects a typo in the PR template, and documents the setup instructions in `README.md`.
Specifically, the following changes are introduced:
- **MCP Configuration**: Created `.agents/mcp_config.json` to register MCP servers for `gitea`, `svelte`, and `cratesio` domains.
- **Documentation**: Updated `README.md` with step-by-step instructions to set up the Go environment, install `cratesio-mcp`, and configure the Gitea access token.
- **Template Correction**: Corrected a typo in `.gitea/PULL_REQUEST_TEMPLATE.yaml`.
- **Git Ignore**: Added `.antigravitycli` directory to `.gitignore` to prevent tracking of local CLI configurations.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#296.
### Checklist
- [x] A milestone is set
- [x] The related issues has been linked to this branch
Reviewed-on: #297
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
This change migrates the frontend environment from Node.js/pnpm to Bun to improve build performance and runtime efficiency. The migration includes updating CI/CD workflows, Docker configurations, and the SvelteKit adapter. Additionally, the PR introduces stricter linting rules via eslint-plugin-simple-import-sort and a global restriction on relative imports to enforce the use of path aliases ($lib/...), ensuring a cleaner and more maintainable import structure across the project. Backend environment variables were also renamed for consistency.
### Package Changes
Package | Action | Version
-- | -- | --
svelte-adapter-bun | Added | 1.0.1
eslint-plugin-simple-import-sort | Added | 13.0.0
@types/node | Added | 22.19.17
eslint-plugin-import | Removed | 2.32.0
@sveltejs/adapter-node | Removed | 5.5.4
svelte | Updated | 5.55.1 -> 5.55.5
@sentry/sveltekit | Updated | 10.47.0 -> 10.51.0
@sveltejs/kit | Updated | 2.55.0 -> 2.58.0
tailwindcss | Updated | 4.2.2 -> 4.2.4
typescript-eslint | Updated | 8.58.0 -> 8.59.1
### Screenshots
_No response_
### Reference
Resolves#290.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #294
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- As the title.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
_No issue_
### Checklist
- [x] A milestone is set
- [ ] The related issuse has been linked to this branch
Reviewed-on: #292
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- As the title
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#288.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #289
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
As the title.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
_No issue_
### Checklist
- [x] A milestone is set
- [ ] The related issuse has been linked to this branch
Reviewed-on: #287
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
#284 Cont.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#282.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #286
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
#284 Cont.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#282.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #285
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
#### Description
This pull request refactors our Sentry configuration to improve error tracking accuracy and support multi-project deployments. We've added explicit release versioning and environment tracking to both the client and server hooks. Additionally, sourcemap uploading has been decoupled from the default SvelteKit Sentry wrapper; we are now directly utilizing `@sentry/vite-plugin` to iterate over an array of projects. This ensures that sourcemaps are correctly uploaded to both the `blog-frontend` and `blog-beta-frontend` Sentry projects during the build process.
#### Key Changes
* **`frontend/src/hooks.client.ts` & `frontend/src/hooks.server.ts`**: Updated `Sentry.init()` to include explicit `release` (`App.__VERSION__`) and `environment` (`process.env.NODE_ENV || 'development'`) tracking.
* **`frontend/vite.config.ts`**:
* Removed inline `sourceMapsUploadOptions` from `sentrySvelteKit()`.
* Imported and configured `@sentry/vite-plugin` to map over an array of `sentryProjects` (`blog-frontend`, `blog-beta-frontend`), ensuring sourcemaps and release versions are tied to multiple target projects during Vite's build phase.
#### Testing/Review Notes
* **Build Verification:** Run a production build (`pnpm build`) and verify that the build output logs confirm sourcemap uploads to both `blog-frontend` and `blog-beta-frontend` Sentry projects.
* **Tag Verification:** Trigger a manual error in your local development environment and check the Sentry dashboard to confirm that the `environment` (e.g., "development") and `release` tags are correctly attached to the event payload.
* **Regression Check:** Ensure that client-side and server-side routing continues to function without Sentry initialization errors.
### Package Changes
* **Added:** `@sentry/vite-plugin` (`^5.2.0`) in `package.json` to handle dedicated Vite build integrations.
* **Updated:** Relevant `@sentry/*` build dependencies (e.g., `babel-plugin-component-annotate`, `bundler-plugin-core`, `rollup-plugin`) bumped to `5.2.0` in `pnpm-lock.yaml` to align with the new Vite plugin addition.
### Screenshots
_No response_
### Reference
Resolves#282.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #284
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
As the title.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
_No issue_
### Checklist
- [x] A milestone is set
- [ ] The related issuse has been linked to this branch
Reviewed-on: #281
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
As the title.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#275.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #280
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
As the title.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#274.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #279
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
As the title.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#273.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #278
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
As the title.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#271.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #277
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
As the title.
### Package Changes
_No response_
### Screenshots

### Reference
Resolves#272.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #276
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
As the title.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#269.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #270
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
As the title.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
_No issue_
### Checklist
- [x] A milestone is set
- [ ] The related issuse has been linked to this branch
Reviewed-on: #268
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
Refer to #265.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#261.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #267
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
#### Summary
This resolves the issue where Sentry logs and source maps were failing to upload during the deployment pipeline (BLOG-261). The core problem was that the frontend build environment lacked the necessary authentication credentials to communicate with the Sentry API. I've updated the deployment workflow to pass the `SENTRY_AUTH_TOKEN` from our CI secrets into the Docker build context, and updated the frontend Dockerfile to securely mount and utilize this token during the build phase. A minor descriptive adjustment was also made to our pre-commit config.
#### Key Changes
* **`.gitea/workflows/deployment.yaml`**: Injected the `SENTRY_AUTH_TOKEN` into the build container's secrets configuration.
* **`frontend/Dockerfile`**: Configured secure secret mounting (`--mount=type=secret`) to read `SENTRY_AUTH_TOKEN` and expose it as an environment variable specifically during the `pnpm run build` execution.
* **`.pre-commit-config.yaml`**: Renamed the `frontend-lint` hook from "frontend lint" to "frontend lint & check" to better reflect its underlying script behavior.
#### Testing/Review Notes
* Trigger a deployment build in the CI/CD pipeline to test the workflow changes.
* Check the build logs for the frontend container; verify that the Sentry plugin successfully detects the token and uploads the sourcemaps/releases without throwing an authentication error.
* Ensure no token leakage occurs in the standard CI output logs or the final compiled Docker image layers.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#261.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #265
Co-authored-by: squid <squid@squidspirit.com>
Co-committed-by: squid <squid@squidspirit.com>
### Description
#### Summary
This PR addresses the ONNX runtime build failures in the backend container. The core issue originates from Alpine Linux's use of `musl` libc, which causes compatibility conflicts with the pre-compiled C/C++ binaries required by the ONNX ecosystem. By migrating both the builder and runner stages to Debian-based slim images (which utilize standard `glibc`), we provide a compatible compilation environment and resolve the build breakage. Furthermore, this update adds the required environment configuration for the Qdrant vector database.
#### Key Changes
* **`backend/Dockerfile`:**
* Switched the base builder image from `rust:1-alpine` to `rust:1-slim`.
* Replaced the `apk` package manager logic with `apt-get` to provision `build-essential`, `libssl-dev`, and `pkg-config` for the Rust build process.
* Updated the runner stage image from `alpine:latest` to `debian:trixie-slim` to ensure runtime compatibility with the compiled `glibc` binary.
* Added `ENV QDRANT_URL=http://127.0.0.1:6334` to expose the vector database endpoint to the server.
#### Testing/Review Notes
* Run a clean build of the backend image locally (`docker build -t backend:test ./backend`) and confirm that the cargo build process bypasses the previous ONNX compilation errors.
* Start the resulting container and verify that the server boots successfully without missing shared library errors (`.so`).
* Ensure your local or CI test environments can reach the specified `QDRANT_URL` or override it as necessary during runtime.
### Package Changes
* **System Packages (Dockerfile):**
* *Removed:* `build-base`, `openssl-dev`, `openssl-libs-static` (Alpine).
* *Added:* `build-essential`, `libssl-dev`, `pkg-config` (Debian).
* **Application Dependencies:** No changes to `Cargo.toml` or `Cargo.lock`.
### Screenshots
_No response_
### Reference
Resolves#262.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #264
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
As the title.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
_No issue_
### Checklist
- [x] A milestone is set
- [ ] The related issuse has been linked to this branch
Reviewed-on: #260
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
#### Description
This PR introduces a significant update to core frontend dependencies, including Svelte, Vite, Tailwind CSS, and Sentry, to keep the tooling up to date. To comply with the updated linting rules and SvelteKit routing standards, all static and interpolated anchor links have been migrated to use the `$app/paths` `resolve` function. Additionally, component reactivity has been modernized to properly leverage Svelte 5 paradigms, replacing static `$state` assignments with lazy-evaluated functions and transitioning direct parameter reads to `$derived` data properties.
#### Key Changes
* **Routing & Path Resolution:** Imported `resolve` from `$app/paths` across navigation, layout, and dashboard components (e.g., `Footer.svelte`, `Navbar.svelte`, `PostPreview.svelte`) to wrap `href` attributes, ensuring correct base path routing. Added `/* eslint-disable svelte/no-navigation-without-resolve */` where necessary.
* **Reactivity Enhancements:** * Migrated `$state` initializations in dialog components (`EditLabelDialog.svelte`, `EditPostDialog.svelte`, `FilteringDialog.svelte`) to use functional wrappers (e.g., `$state((() => defaultValues)())`) to ensure proper evaluation.
* Updated components and pages to utilize `$derived(data.id)` instead of extracting directly from `params`, accompanied by corresponding `id` returns in `+page.server.ts` load functions.
* Refactored initial data population in root page components to use getter functions (`getInitialData()`) before passing them to container stores.
* **UI/Layout Updates:** * Changed container max-widths from `max-w-screen-xl` to `max-w-7xl` in `Footer.svelte`, `Navbar.svelte`, and `NotFoundPage.svelte`.
* Added `rel="external"` attributes to all outbound links in the footer.
* Replaced hardcoded policy link constants with dynamic parameterized routes.
* **Store Interfaces:** Formatted implementation signatures for improved readability in `postsListedStore.ts`.
#### Testing/Review Notes
* Run `pnpm install` before testing to synchronize the massive updates in `pnpm-lock.yaml`.
* Navigate through the application (especially the dashboard and policy links in the footer) to verify that `$app/paths` `resolve` accurately handles all internal routing.
* Test the `EditPostDialog` and `EditLabelDialog` components to verify that default form values initialize and persist correctly under the new functional `$state` declarations.
* Confirm external links in the footer open correctly without triggering router intercepts.
### Package Changes
* **Updated:** * `svelte` (`5.39.13` -> `5.55.1`)
* `@sveltejs/kit` (`2.22.0` -> `2.55.0`)
* `vite` (`7.0.4` -> `7.3.1`)
* `tailwindcss` (`4.0.0` -> `4.2.2`)
* `eslint` (`9.18.0` -> `9.39.4`)
* `@sentry/sveltekit` (`10.1.0` -> `10.47.0`)
* `typescript-eslint` (`8.20.0` -> `8.58.0`)
* Various other `@sveltejs/*`, `@tailwindcss/*`, and minor tooling dependencies bumped to their latest compatible versions.
### Screenshots
_No response_
### Reference
Resolves#253.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #259
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
#### Summary
This PR introduces a Redis-backed caching layer for search query embeddings. By caching the vector embeddings of search strings, we eliminate redundant embedding generation for repeated queries, significantly improving search latency and reducing CPU/compute overhead. Additionally, the Redis connection management has been refactored to use `deadpool-redis`, establishing a shared connection pool that now serves both the newly implemented embedding cache and the existing Actix session store.
#### Key Changes
* **Search Caching Logic:** Introduced the `SearchQueryEmbeddingCache` trait and implemented `SearchQueryEmbeddingCacheRedisImpl`. Cache keys are uniquely bound to the specific model revision to prevent vector dimension mismatches if the underlying embedding model is updated.
* **Repository Integration:** Modified `SearchRepositoryImpl` to check the Redis cache before calling the text embedding model. Cache misses trigger embedding generation and automatically populate the cache.
* **Redis Pooling:** Abstracted Redis configuration into a dedicated `RedisConfiguration` struct utilizing `deadpool_redis::Pool`.
* **Session Store Refactor:** Updated `actix-session` to use `RedisSessionStore::builder_pooled()`, migrating it to the shared Redis connection pool instead of managing its own standalone connection.
* **Configuration & Env:** Added `REDIS_SESSION_PREFIX` and `REDIS_SEARCH_QUERY_EMBEDDING_PREFIX` to `.env.example` for namespace isolation.
* **Frontend Copy:** Updated the search placeholder text in `frontend/src/lib/strings.ts` to clarify that the search uses vector comparison.
#### Testing/Review Notes
* Ensure a local Redis instance is running and accessible via `REDIS_URL`.
* Execute a semantic search query. Verify via `redis-cli` that a new cache key is generated matching the pattern configured by the prefix and model revision (e.g., `search:query_embedding:<model>:<query>`).
* Execute the same search query a second time. Verify that the search executes noticeably faster and bypasses the local embedding generation phase.
* Verify that user authentication and session states remain fully functional to ensure the `actix-session` pool refactor introduced no regressions.
### Package Changes
* **Added:** `deadpool-redis` (`0.22.1`), `deadpool` (`0.12.3`), `deadpool-runtime` (`0.1.4`)
* **Added:** `redis` (`0.32.7` with features: `aio`, `connection-manager`, `tokio-comp`)
* **Added:** `serde_json` (`1.0.145`) to `backend/Cargo.toml` and `backend/feature/search/Cargo.toml`
* **Updated:** `actix-session` (`0.11.0`) - Added the `redis-pool` feature.
### Screenshots
_No response_
### Reference
Resolves#246.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #257
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
### Summary
This PR replaces the legacy string-matching search logic with an AI-powered semantic vector search (Retrieval-Augmented Generation) for blog posts. By integrating Qdrant as the vector database and FastEmbed for generating local text embeddings, the application now allows users to perform context-aware semantic searches or ask natural language questions to find relevant post content. The backend text processing leverages markdown splitting and tokenization to chunk post content efficiently. The frontend has been updated to provide UI hints regarding this new AI search capability, utilizing a newly added Tooltip component.
### Key Changes
* **Backend - New Search Module:** Created a new `search` workspace feature containing `SearchRepository`, `SearchVectorDbService`, and `SearchService`.
* **Backend - Post Integration:** Integrated `SearchService` into `CreatePostUseCase`, `UpdatePostUseCase`, and `GetAllPostInfoUseCase`. Post creations and updates now automatically trigger vector document indexing. Retrieving posts with a keyword query now delegates to the vector search to return contextually relevant post IDs.
* **Backend - Embeddings & Vector DB:** Added dependency injection and configurations for Qdrant (`QdrantConfiguration`) and local embeddings (`EmbeddingConfiguration`), utilizing the `BAAI/bge-large-zh-v1.5` model for optimized Chinese text embeddings.
* **Backend - Environment & Build:** Added required Qdrant and embedding environment variables to `.env.example`. Updated the `Dockerfile` to utilize parallel compilation (`cargo build --release -j2`) for faster build times.
* **Frontend - UI Components:** Implemented a new `Tooltip` component architecture using `bits-ui`.
* **Frontend - User Experience:** Updated the `FilteringDialog` to display a tooltip hint explaining the new RAG-based semantic search to users. Updated `strings.ts` with relevant placeholders and instructions.
### Testing/Review Notes
* Ensure a local Qdrant instance is running and accessible at the URL defined in your `.env` (default is `http://127.0.0.1:6334`).
* On initial backend startup, verify that the `BAAI/bge-large-zh-v1.5` embedding model downloads successfully to your designated `EMBEDDING_CACHE_DIR`.
* Create or edit an existing post in the system. Check your Qdrant dashboard/API to verify that the post's content chunks are successfully vectorized and upserted into the `blog_post` collection.
* Perform a search query using natural language in the frontend to confirm semantic retrieval returns the expected posts.
* Verify that the new tooltip renders correctly when hovering over the info icon in the search dialog.
### Package Changes
* **Backend (`Cargo.toml` / `Cargo.lock`):**
* **Added:** `fastembed` (5.13.0), `qdrant-client` (1.17.0), `text-splitter` (0.29.3, with `markdown` and `tokenizers` features), `tokenizers` (0.22.2).
* **Updated:** `tokio` (from 1.50.0 to 1.51.0).
* **Frontend (`package.json` / `pnpm-lock.yaml`):**
* **Updated:** `bits-ui` (from ^2.11.5 to ^2.16.5), `@internationalized/date` (from ^3.10.0 to ^3.12.0).
### Screenshots
_No response_
### Reference
Resolves#237.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #256
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
### Summary
This pull request refactors the static `ErrorPage` component into a dynamic, reusable `NotFoundPage`. By accepting a `title` prop, the component can now handle various "not found" or empty states across the application rather than just standard HTTP 404 errors. Notably, this implementation is now utilized in the `PostOverallPage` to gracefully handle scenarios where no posts are returned (e.g., empty database or zero search filter results). Minor structural UI adjustments and centralized string constants were also added to support these updates.
### Key Changes
* **`frontend/src/lib/common/framework/ui/NotFoundPage.svelte`**:
* Renamed from `ErrorPage.svelte`.
* Added a `title` prop to replace hardcoded '404' strings.
* Updated flexbox utility classes (`flex-wrap`, `shrink-0`) to improve layout responsiveness on smaller viewports.
* **`frontend/src/lib/post/framework/ui/PostOverallPage.svelte`**:
* Introduced an `isPostsEmpty` derived state.
* Added conditional rendering to display the `NotFoundPage` with an `EMPTY_POSTS` string when no articles are available.
* **`frontend/src/lib/strings.ts`**:
* Added new constants: `EMPTY_POSTS` ('查無文章'), `NOT_FOUND_CODE` ('404'), and `POST` ('文章') to centralize copy.
* **`frontend/src/routes/+error.svelte` & `frontend/src/routes/dashboard/+layout.svelte`**:
* Replaced legacy `ErrorPage` imports with `NotFoundPage`.
* Passed `Strings.NOT_FOUND_CODE` as the required title prop to maintain previous 404 behavior.
* **`frontend/src/lib/post/framework/ui/FilteringButton.svelte`**:
* Changed the wrapper element from a `<button>` to a `<div>`.
###Testing/Review Notes
* Navigate to an invalid URL to ensure the global `+error.svelte` routing still correctly renders the "404" page.
* Visit the main posts page (`/post`). Apply a filter (keyword or label) that is guaranteed to return zero results. Verify that the new "Empty Posts" (查無文章) view is rendered correctly and the layout does not break.
* Verify that unauthenticated access to the dashboard still triggers the standard 404 view via the layout wrapper.
### Package Changes
_No response_
### Screenshots
<video src="attachments/93d87bc9-48d0-4d50-bb29-f335ecfa0cdb" title="螢幕錄影 2026-04-02 14.58.18.mov" controls></video>
### Reference
Resolves#236.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #255
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
This pull request introduces dynamic markdown-based pages for site policies and refactors the global Footer component to improve maintainability and responsive design. The hardcoded static links and UI sections within the Footer were extracted into centralized configuration classes (`Links` and `Strings`) and modular Svelte snippets. Furthermore, we established a new dynamic route `/terms/[name]` capable of serving static markdown files, specifically introducing the Privacy Policy and AI Usage Policy to the platform.
### Key Changes
* **`frontend/src/lib/common/framework/ui/Footer.svelte`:** Upgraded the script block to TypeScript. Refactored the markup using Svelte `#snippet` blocks (`desktopLayout`, `mobileLayout`, `icons`, `policies`, etc.) for cleaner component structure. Integrated centralized strings and links.
* **`frontend/src/lib/links.ts`:** Created a new centralized abstraction for external application URLs (YouTube, Email, RSS, Source Code).
* **`frontend/src/lib/strings.ts`:** Enforced `string` types on existing constants and added new centralized UI strings (`PRIVICY_POLICY`, `AI_USAGE_POLICY`, `TOC`).
* **`frontend/src/lib/post/framework/ui/PostContentPage.svelte`:** Replaced hardcoded "Table of Contents" text with the newly centralized `Strings.TOC` property.
* **`frontend/src/routes/terms/[name]/+page.ts` & `+page.svelte`:** Implemented a dynamic route parameter that fetches corresponding markdown files from the static directory and safely renders them using the existing `MarkdownRenderer` component. Throws a 404 if the markdown file is absent.
* **`frontend/static/terms/*.md`:** Added `privacy-policy.md` and `ai-usage-policy.md` assets containing the platform's official policies.
### Testing/Review Notes
* Verify the global Footer renders correctly on both mobile and desktop viewports.
* Click through the external links in the Footer (YouTube, Email, RSS, Git) to ensure they route to the appropriate targets.
* Navigate to `/terms/privacy-policy` and `/terms/ai-usage-policy` to confirm the markdown documents are fetched and rendered properly.
* Manually test an invalid terms route (e.g., `/terms/does-not-exist`) to verify the 404 error handling functions as expected.
* Open a standard post page to ensure the Table of Contents header correctly displays the localized string.
### Package Changes
_No response_
### Screenshots
|Footer|Terms|
|-|-|
|||
### Reference
Resolves#245.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #254
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
This PR introduces HTTP caching mechanisms to the image retrieval endpoint to optimize bandwidth consumption and improve client-side load times. By implementing `ETag` generation and validation alongside aggressive `Cache-Control` directives, the server can now negotiate caching with the client. If the client holds a valid cached version of the image, the server will return a `304 Not Modified` response, entirely bypassing the need to re-transmit the binary image payload.
#### Key Changes
* **`backend/feature/image/src/framework/web/get_image_by_id_handler.rs`:**
* Modified `get_image_by_id_handler` to accept `HttpRequest` for header inspection.
* Added conditional logic to evaluate the `If-None-Match` header against a newly generated ETag.
* Injected `ETag` and `Cache-Control` (`public, max-age=31536000, immutable`) headers into both `200 OK` and `304 Not Modified` responses.
* Implemented `is_not_modified` helper function to accurately parse and validate `If-None-Match` candidates, including wildcard (`*`) support.
* Implemented `generate_etag` utilizing the `Fnv64` hashing algorithm for highly performant, non-cryptographic hashing of the image binary data.
#### Testing/Review Notes
1. Initiate a `GET` request to retrieve an image by its ID. Confirm the response returns a `200 OK` status, includes the binary payload, and exposes both `ETag` and `Cache-Control` headers.
2. Copy the returned `ETag` value.
3. Initiate a secondary `GET` request for the same image ID, explicitly passing the copied ETag value within the `If-None-Match` HTTP header.
4. Verify that the server responds with a `304 Not Modified` status code, omitting the image body payload, while still returning the correct caching headers.
### Package Changes
* **Added:** `fnv_rs` (`v0.4.4`) - Introduced for high-performance FNV-1a hashing required during ETag computation.
* **Added:** `paste` (`v1.0.15`) - Transitive dependency resolved via `fnv_rs`.
### Screenshots

### Reference
Resolves#240.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #252
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
This PR introduces the ability to delete uploaded images while ensuring data integrity by preventing the deletion of images currently referenced by any posts. It adds a new `post_image` relational table to track these references dynamically. The frontend has been updated to support deleting images from the dashboard, visually disabling the deletion action for in-use images. Additionally, we've extracted image parsing logic into a dedicated service, updated dependencies across the backend, and strengthened our pre-commit hooks to ensure deeper code validation.
#### Key Changes
* **Database Migrations**:
* Created the `post_image` join table to establish a relationship between posts and images and track active references.
* **Backend - Image Module**:
* Added `DeleteImageUseCase` and `delete_image_handler` (`DELETE /image/{id}`) to handle safe deletions.
* Refactored the internal entity `ImageInfo` to `ImageMetaData` and introduced an `is_referred` boolean on API DTOs.
* Updated `GetImageInfoUseCase` and `ListImagesUseCase` to query the `PostRepository` for reference counts, determining if an image is safe to delete.
* **Backend - Post Module**:
* Introduced `ImageExtractor` to automatically parse Markdown content and the `preview_image_url` to extract embedded image IDs.
* Updated `CreatePostUseCase`, `UpdatePostUseCase`, and `PostDbServiceImpl` to synchronize `post_image` relationships upon post creation/updates based on the extracted IDs.
* Refactored label validation logic into a centralized `LabelRelationService`.
* **Frontend**:
* Implemented `ImageDeletedStore` and the corresponding `DeleteImageUseCase`.
* Updated `ImageOverallDashboardPage` to display a delete button, which disables automatically if `isReferred` is true.
* Changed `previewImageUrl` types from `URL` to `string` in post DTOs, ViewModels, and UI components (`OpenGraph`, `StructuredData`) to simplify state hydration/serialization.
* **Tooling & CI**:
* Updated `.pre-commit-config.yaml` to restrict hook executions to their specific directories (`^backend/` and `^frontend/`).
* Added `cargo test` to `backend-check.sh` and `pnpm check` to `frontend-lint.sh`.
#### Testing/Review Notes
* **Database setup**: Run `sqlx migrate run` to apply the new `post_image` table migration.
* **Upload & Reference**: Upload an image via the dashboard. Create a new post referencing the image (either within the Markdown content or as the preview image).
* **Deletion constraint check**: Navigate to the Image Dashboard. Verify the delete button for the referenced image is disabled. Sending a direct `DELETE` request to the API should return a `400 Bad Request` with the `ReferencedImage` error.
* **Successful deletion**: Remove the image reference from the post (or delete the post). Return to the Image Dashboard and verify the image can now be successfully deleted.
* **Tooling check**: Verify that committing changes locally correctly runs `cargo test` and `pnpm check` without failures.
### Package Changes
**Backend (`Cargo.toml` / `Cargo.lock`)**:
* Updated `actix-web` from **4.12.1** to **4.13.0**
* Updated `tokio` from **1.48.0** to **1.50.0**
* Updated `chrono` from **0.4.42** to **0.4.44**
* Updated `anyhow` from **1.0.100** to **1.0.102**
* Updated `env_logger` from **0.11.8** to **0.11.10**
* Updated `futures` from **0.3.31** to **0.3.32**
* Updated `regex` from **1.12.2** to **1.12.3**
* Updated `sentry` from **0.46.0** to **0.47.0**
* Various transitive dependencies updated to reflect the new primary bumps.
### Screenshots
|Is Not Referred|Is Referred|
|-|-|
|||
### Reference
Resolves#243.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #250
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
#### Description/Summary
This pull request streamlines our frontend UI architecture by removing the deprecated `SearchBar.svelte` component, pointing toward a consolidation of search input handling. To maintain a consistent and polished user experience, the keyword input within `FilteringDialog.svelte` has been updated with refined padding and placeholder typography. Additionally, a root `.gitignore` file has been initialized to establish standard exclusions for environment configurations and OS-specific artifacts, preventing accidental commits of sensitive or unnecessary files.
#### Key Changes
* **`.gitignore`:** Added root ignore file to exclude `.env` configurations and macOS `.DS_Store` artifacts from version control.
* **`FilteringDialog.svelte`:** Enhanced the `Input` component by injecting utility classes (`px-4 py-2 text-sm placeholder:text-gray-500`) for improved spatial alignment, sizing, and placeholder legibility.
* **`SearchBar.svelte`:** Deleted the component entirely, including its localized debounce logic, state management, and DOM elements, to reduce technical debt and component duplication.
### Package Changes
_No response_
### Screenshots

### Reference
Resolves#241.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #249
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
Make the number sync with real year number.
### Package Changes
_No response_
### Screenshots

### Reference
Resolves #
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #248
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
Fix by using `Environment` attribute instead of from `window` api.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolve#238.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #239
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
As the title
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
_No issue_
### Checklist
- [x] A milestone is set
- [ ] The related issuse has been linked to this branch
Reviewed-on: #234
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
**Description**
This PR introduces a comprehensive filtering and search mechanism for the blog post list. It replaces the simple search bar with a robust filtering dialog that supports both keyword search and label selection. To enhance the mobile user experience, the `vaul-svelte` library has been integrated to provide native-feeling drawer interactions. Additionally, the filtering state is now synchronized with URL query parameters, allowing for shareable filtered views.
**Key Changes**
* **Dependencies:**
* Added `vaul-svelte` (v1.0.0-next.7) for handling mobile drawer interactions.
* **UI Components:**
* Implemented a set of generic `Drawer` components (`drawer-content`, `drawer-overlay`, etc.) wrapping `vaul-svelte`.
* Renamed the existing application navigation drawer from `Drawer.svelte` to `NavigationDrawer.svelte` to avoid namespace conflicts.
* Updated `Footer.svelte` to use the semantic `<footer>` tag.
* **Filtering Feature:**
* **FilteringDialog:** Created a responsive form that renders as a `Dialog` on desktop and a `Drawer` on mobile. Includes inputs for keywords and a combobox for label selection.
* **FilteringButton:** Added a floating action button (FAB) that adjusts its position relative to the footer visibility. It displays a "rainbow ring" animation when active filters are applied.
* **PostOverallPage:** Replaced `SearchBar` with `FilteringDialog`. Implemented logic to sync form state with URL search parameters (`keyword`, `label_id`).
* **Data & Logic:**
* Updated `routes/post/+page.server.ts` and `+page.svelte` to parse query parameters and hydrate both post and label stores server-side.
* Added `setData` method to `PostsListedStore`.
* Updated `PostContentPage` to link post tags directly to the filtered list view.
**Testing & Review Notes**
1. **Functionality:**
* Navigate to the post list page. Verify that clicking the new floating search button opens the filtering modal.
* Test filtering by keyword, label, or both. Ensure the list updates correctly.
* Verify that applying a filter updates the URL (e.g., `?keyword=test&label_id=1`). Refreshing the page should persist the filtered state.
2. **Responsiveness:**
* **Desktop:** Ensure the filter opens as a centered Dialog.
* **Mobile:** Ensure the filter opens as a bottom-sheet Drawer.
3. **Regression:**
* Verify that the main navigation menu (`NavigationDrawer`) still functions correctly after the rename.
* Check that the footer layout remains stable.
### Package Changes
```
"vaul-svelte": "1.0.0-next.7",
```
### Screenshots
|Scenario|Mobile|Desktop|
|-|-|-|
|Inactive||<img width="1680" alt="截圖 2025-12-16 01.45.58.png" src="attachments/4b478cda-33d4-4a9a-beb4-9cd4e5795350">|
|Active||<img width="1680" alt="截圖 2025-12-16 01.43.57.png" src="attachments/30a02bbb-7915-484b-95cc-371e951c666d">|
|Configuration||<img width="1680" alt="截圖 2025-12-16 01.43.34.png" src="attachments/17fa73b6-5f87-4781-987b-484db5de7e91">|
### Reference
Resolves#199.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #231
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
#### Summary
This PR introduces a new `script/toolbox` directory containing a Python-based utility service designed to handle background tasks, specifically generating an RSS 2.0 feed (`generate_rss.py`). It includes the necessary Docker infrastructure to build and deploy this toolbox alongside the existing services.
Additionally, this change refactors the location of maintenance shell scripts to `script/pre-commit/` for better organization and optimizes the CI/CD deployment workflow by implementing Docker layer caching and removing redundant frontend build steps.
#### Key Changes
##### Toolbox & RSS
* **New Service:** Added `script/toolbox/` containing a Python 3.14 environment, `Dockerfile`, and `requirements.txt`.
* **RSS Logic:** Implemented `generate_rss.py` to fetch published posts from the PostgreSQL database, scrape site metadata, and generate a `feed.xml` file.
* **Documentation:** Added `README.md` and `.env.example` for the toolbox service.
##### CI/CD & Infrastructure
* **Workflow Update (`deployment.yaml`):**
* Added a new job to build and push the `toolbox` Docker image.
* Enabled `cache-from` and `cache-to` (registry caching) for Frontend, Backend, and Toolbox images to speed up build times.
* **Frontend CI:** Removed the redundant `pnpm run build` step from `frontend-ci.yaml`.
##### Refactoring & UI
* **Script Reorganization:** Moved `backend-check.sh`, `frontend-lint.sh`, and `sqlx-prepare.sh` into `script/pre-commit/` and updated their internal path resolution.
* **Pre-commit:** Updated `.pre-commit-config.yaml` to reflect the new script locations.
* **Frontend UI:** Added an RSS icon link to `Footer.svelte` pointing to the generated feed.
#### Testing & Review Notes
1. **Toolbox Build:** Verify the `toolbox` image builds successfully:
```bash
cd script/toolbox
docker build -t blog-toolbox .
```
2. **RSS Generation:**
* Run the container locally with correct `.env` variables.
* Execute `python generate_rss.py` inside the container.
* Verify the output `feed.xml` validates against RSS 2.0 standards.
3. **Pre-commit Hooks:** Run `pre-commit run --all-files` to ensure the path refactoring hasn't broken local linting/checking.
4. **Deployment:** Monitor the next Gitea Action run to confirm registry caching is functioning correctly.
### Package Changes
```
python-dotenv==1.2.1
psycopg2-binary==2.9.11
requests==2.32.5
```
### Screenshots
> Screenshot at Feeder app on Android
<img src="/attachments/d791ac12-307c-43a7-a146-c423f2600c02" width="200px"/>
### Reference
Resolve#227.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #230
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
## 📝 Summary
This PR implements the ability to search/filter posts by keyword. It introduces a search bar on the frontend and passes the search query through the architectural layers to the backend, where a basic title-based filtering is currently applied.
## 🛠️ Changes
### Backend (`backend/feature/post`)
- **API & Controller**: Updated `PostController` and `PostQueryDto` to accept an optional `keyword` parameter.
- **Use Case**: Updated `GetAllPostInfoUseCase` to propagate the `keyword` to the repository.
- **Repository**:
- Updated `PostRepository` interface and implementation.
- **Logic**: Implemented a temporary in-memory filter that checks if the `post.title` contains the keyword.
- *Note*: Added a TODO to replace this string matching with vector/embedding search in the future.
### Frontend (`frontend/src/lib/post`)
- **UI Components**:
- Created a new component `SearchBar.svelte` with:
- Input debouncing (1000ms).
- Clear button functionality.
- Hover/Focus styling effects.
- Integrated `SearchBar` into `PostOverallPage.svelte`.
- **State Management**: Updated `PostsListedStore` to handle the `keyword` trigger.
- **Gateway/API**: Updated `PostListQueryDto` and `PostRepositoryImpl` to append the `keyword` to the URL search parameters when calling the backend.
## ⚠️ Technical Notes
- **Search Scope**: Currently, the search is **strictly limited to the post title**. The logic is explicitly filtering: `post.title.contains(keyword)`.
- **Performance**:
- The frontend implements a **1-second debounce** on the input to reduce unnecessary API calls.
- The backend filtering happens *after* fetching post info from the DB service (in `map` logic). As indicated in the code comments, this is a provisional solution pending a switch to embedding-based search.
### Package Changes
_No response_
### Screenshots


### Reference
Resolve#198.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #229
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
## Feature: Implement Image Gallery and Detail Views
### Summary
This PR introduces full functionality for listing images and viewing image details. It extends the backend to support retrieving image metadata without binary data and updates the frontend to display a gallery grid and a dedicated detail page. Additionally, it enforces absolute imports in the frontend codebase via updated ESLint rules.
### 🏗️ Backend Changes (`/backend`)
**New Endpoints**
* **`GET /image`**: Retrieves a list of all non-deleted images (metadata only: ID and MIME type).
* **`GET /image/{id}/info`**: Retrieves specific metadata for an image ID without fetching the binary blob.
**Architecture & Refactoring**
* **Domain Entities**: Split the concept of an image into `ImageInfo` (metadata) and `Image` (contains `ImageInfo` + binary data). This allows listing images efficiently without loading heavy binary data from storage.
* **Use Cases**: Added `ListImagesUseCase` and `GetImageInfoUseCase`.
* **Repository**: Updated `ImageRepository` and `ImageDbService` to support listing and metadata retrieval using new SQLx queries.
* **API**: Updated `openapi` documentation to include the new routes.
### 🖥️ Frontend Changes (`/frontend`)
**UI & Components**
* **Image Dashboard (`/dashboard/image`)**:
* Implemented a grid layout gallery.
* Added hover effects showing Image ID and MIME type.
* Added quick actions: **Copy URL** and **View Details**.
* **Image Details (`/dashboard/image/[id]`)**:
* Created a dedicated page to view full-size images.
* Displays a table with ID, MIME Type, and URL (with copy functionality).
* **Utilities**: Added `copyToClipboard.ts` utility with Toast notification integration.
**State Management**
* Added `ImagesListedStore` to handle fetching and state for the gallery.
* Added `ImageLoadedStore` to handle fetching state for the detail view.
* Updated `Container` to inject these new dependencies.
**Developer Experience (DX)**
* **ESLint**: Added `eslint-plugin-import`.
* **Rules**: Enforced absolute imports (`import/no-relative-parent-imports`) to maintain a cleaner project structure (CSS files are exempted).
-----
### 🔍 Technical Details
#### 1\. Optimization: Splitting Metadata vs. Data
To prevent high memory usage and latency when listing images, the backend now distinguishes between fetching *info* and fetching *blobs*.
#### 2\. ESLint Configuration
New rules now prevent `../../` spaghetti imports in TypeScript files.
```javascript
// Invalid
import { foo } from '../../common/foo';
// Valid
import { foo } from '$lib/common/foo';
```
-----
### 🧪 Testing
1. **Gallery View**:
* Navigate to `/dashboard/image`.
* Verify that previously uploaded images appear in the grid.
* Hover over an image and click the "Copy" icon; verify the URL is in your clipboard.
2. **Detail View**:
* Click the "Eye" icon on an image card.
* Verify redirection to `/dashboard/image/[id]`.
* Verify the image loads and metadata matches.
3. **Upload**:
* Upload a new image via the dashboard.
* Verify the list updates automatically upon success.
### Package Changes
"eslint-plugin-import": "^2.32.0",
### Screenshots


### Reference
Resolve#158.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #228
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- As the title
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
_No issue_
### Checklist
- [x] A milestone is set
- [ ] The related issuse has been linked to this branch
Reviewed-on: #225
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- Make structured data be able to be passed different type of data.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#202.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #222
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
Merge [logo-light.svg](/frontend/static/icon/logo-light.svg) and [logo-dark.svg](/frontend/static/icon/logo-light.svg) as a single [favicon.svg](/frontend/static/favicon.svg)
### Package Changes
_No response_
### Screenshots
|Dark Mode|Light Mode|
|-|-|
|||
### Reference
Resolves#219.
https://gemini.google.com/share/75ba68b088e3
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #221
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- As the title
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
_No issue_
### Checklist
- [x] A milestone is set
- [ ] The related issuse has been linked to this branch
Reviewed-on: #218
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- As the title
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#216.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #217
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- As the title.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#210.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #215
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- As the title
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#210.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #214
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- Using dynamic rendering
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#210.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #213
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- As the title.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
_No issue_
### Checklist
- [x] A milestone is set
- [ ] The related issuse has been linked to this branch
Reviewed-on: #212
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- Move all `<svelte:head>` into +page.svelte component
- Testing with selenium `--headless` option
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#210.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #211
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- As the title.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
_No issue_
### Checklist
- [x] A milestone is set
- [ ] The related issuse has been linked to this branch
Reviewed-on: #209
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- As the title
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#207.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #208
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- As the title.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
_No issue_
### Checklist
- [x] A milestone is set
- [ ] The related issuse has been linked to this branch
Reviewed-on: #204
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- Place json-ld script into <svelte:head> section
### Package Changes
_No response_
### Screenshots

### Reference
Resolves#201.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #203
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- As the title.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
_No issue_
### Checklist
- [x] A milestone is set
- [ ] The related issuse has been linked to this branch
Reviewed-on: #196
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- As the title.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#194.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #195
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- As the title
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
_No issue_
### Checklist
- [x] A milestone is set
- [ ] The related issuse has been linked to this branch
Reviewed-on: #191
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- Fix by removing unused environment variables from backend and frontend Dockerfiles. They have been set on hosting machine.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#163.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #189
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- Fix by adding `flex-wrap` on the lablels div
### Package Changes
_No response_
### Screenshots
|Original|Fixed|
|-|-|
|||
### Reference
Resolves#187.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #188
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- Enhance navbar hiding behavior
- Hide when scrolling down
- Show when scrolling up
- Add drawer on mobile device to show action links and toc
- Improve toc size on desktop
- Fix unsafe html tag
### Package Changes
_No response_
### Screenshots
|Scenario|Screenshot|
|-|-|
|Toggling drawer on home page|<video src="attachments/96879052-cdd5-46bf-be23-352bd04118b2" title="螢幕錄影 2025-10-23 22.28.30.mov" controls></video>|
|Click links on drawer|<video src="attachments/bb574a5d-0307-4b99-abec-5741a4ef16b1" title="螢幕錄影 2025-10-23 22.29.43.mov" controls></video>|
|Interating with toc on drawer|<video src="attachments/96cb8aa4-eeb3-498c-b5af-7817d7f124c1" title="螢幕錄影 2025-10-23 22.30.38.mov" controls></video>|
|Navbar auto hiding|<video src="attachments/ca6ff6ff-e755-4f6e-9e20-28b9060de6f1" title="螢幕錄影 2025-10-23 22.31.34.mov" controls></video>|
### Reference
Resolves#161.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #186
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- It supports the following operations through UI:
- Search and add a label
- Remove a label
- Move the label previous or next
- Preserve the original input element for restoring
### Package Changes
_No response_
### Screenshots
<video src="attachments/95f34d52-b170-4782-8067-915cd757c9be" title="螢幕錄影 2025-10-23 05.14.53.mov" controls></video>
### Reference
Resolves#151.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #185
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
Change the input data structure of each back-end feature (auth, image, label, post) from using Domain Entity to using a dedicated Params structure, and move errors from the application layer to the domain layer.
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#181.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #182
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- Using `highlight.js` for highlighting
- Allow `class` attribute for `<pre>` and `<span>` in `sanitizeHtml`
### Package Changes
```json
{
"highlight.js": "^11.11.1"
}
```
### Screenshots
|Plain Text|Rust|
|-|-|
|||
### Reference
Resolves#168.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #179
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
> [!NOTE]
> New frontend environment: `PUBLIC_APP_BASE_URL`, which is frontend url.
### Package Changes
_No response_
### Screenshots
https://ogcheck.com/zh-hant

### Reference
Resolves#172.
See also: https://ogp.me/
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #178
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- Using shadcn hover card to implement.
### Package Changes
_No response_
### Screenshots

### Reference
Resolves#173, and also fixes#164.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #175
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- As the title
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
_No Response_
### Checklist
- [x] A milestone is set
- [ ] The related issuse has been linked to this branch
Reviewed-on: #162
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- Restrict `w` to fix the issue
### Package Changes
_No response_
### Screenshots

### Reference
Resolves#159.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #160
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- Removed semantic_id field from backend and frontend updating dto
### Package Changes
_No response_
### Screenshots
_No response_
### Reference
Resolves#155.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #157
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
### Description
- Only support for desktop device
### Package Changes
```json
{
"@types/crypto-js": "^4.2.2",
"crypto-js": "^4.2.0",
"svelte": "^5.39.13",
}
```
### Screenshots
<video src="attachments/602a4934-532c-4f03-9202-9e5909718964" title="螢幕錄影 2025-10-17 清晨6.57.27.mov" controls></video>
### Reference
Resolves#89.
### Checklist
- [x] A milestone is set
- [x] The related issuse has been linked to this branch
Reviewed-on: #153
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>