Compare commits

..

5 Commits

Author SHA1 Message Date
4a0bd62432 chore: update README
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m12s
2026-04-11 19:53:32 +02:00
d409928506 chore: update README
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m32s
2026-04-11 19:47:12 +02:00
f1e2231675 feat(umami): setup
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m32s
2026-02-27 20:07:00 +01:00
e87f19e7ce feat(api): allow _ and - in logo name
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m35s
2026-02-22 14:07:22 +01:00
3d53660319 feat(api): normalize logo name 2026-02-22 14:06:15 +01:00
6 changed files with 98 additions and 48 deletions

View File

@@ -1,36 +1,47 @@
<h1 align="center">
<br>
<img src="https://i.imgur.com/an3wOdO.png" alt="QRCode Image" width="200">
<br>
simple-qr.com
<br>
</h1>
<div align="center">
<h1><a target="_blank" href="https://simple-qr.com">simple-qr.com</a></h1>
<img src="./docs/demo.gif" alt="demo" />
</div>
<h4 align="center">Simple, bullshit-free QR code generator with straightforward API.</h4>
Simple, bullshit-free QR code generator with straightforward API.
<p align="center">
<a href="https://nuxt.com">
<img src="https://img.shields.io/badge/nuxt-4ade80?style=for-the-badge&logo=vite&logoColor=white">
</a>
<a href="https://typescriptlang.org">
<img src="https://img.shields.io/badge/TypeScript-007acc?style=for-the-badge&logo=typescript&logoColor=white">
</a>
</p>
## What it does
<p align="center" id="links">
<a href="#description">Description</a> •
<a href="https://simple-qr.com">Visit it</a> •
<a href="#license">License</a>
</p>
- Generate QR codes via a simple web UI or a straightforward REST API
- Embed logos (30+ brands: Gitea, Signal, Monero, Session, etc.) in the center of QR codes
- Export in PNG, JPEG, or WebP formats
- No authentication, no rate limiting, no ads
<br>
## Stack
## Description
- [Nuxt 4](https://nuxt.com) + [Nuxt UI](https://ui.nuxt.com)
I created this side project to learn more about Nuxt and Vue, also because I needed a simple way to generate QR codes, but all websites I found were full of ads and were over complicated.
## API
<br>
### `GET /api` - Generate a QR code
## License
Returns the QR code as an image file.
This project is <a href="https://opensource.org/licenses/MIT">MIT</a> licensed.
| Parameter | Type | Required | Description |
|-----------|---------------------------|----------|-----------------------------------------------------|
| `content` | string | yes | Data to encode |
| `format` | `png` \| `jpeg` \| `webp` | no | Output format (default: `png`) |
| `logo` | string | no | Logo name to embed in the center (case-insensitive) |
**Example:**
```
GET https://simple-qr.com/api?content=https://git.pihkaal.me&format=webp&logo=gitea
```
### `GET /api/logos` - List available logos
Returns a JSON array of available logo names.
**Example:**
```
GET https://simple-qr.com/api/logos
```
```json
["signal", "monero", "session", ...]
```

View File

@@ -11,6 +11,7 @@ services:
- "traefik.http.services.simple-qr.loadbalancer.server.port=3000"
- "traefik.http.routers.simple-qr.tls=true"
- "traefik.http.routers.simple-qr.tls.certResolver=myresolver"
- "traefik.http.routers.simple-qr.middlewares=umami-middleware@file"
restart: always
networks:

BIN
docs/demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 KiB

View File

@@ -1,4 +1,8 @@
import { resolve } from "path";
import type { Canvas } from "skia-canvas";
import { getLogoNames } from "../utils/logos";
const normalize = (s: string) => s.toLowerCase().replace(/[\s_-]+/g, "");
export default defineEventHandler(async (event) => {
const { format, logo, content } = await getValidatedQuery(
@@ -6,8 +10,26 @@ export default defineEventHandler(async (event) => {
settingsSchema.parse,
);
const logoUrl = logo ? resolve("public", `logos/${logo}.png`) : undefined;
const canvas = await renderQRCodeToCanvas(content, logoUrl);
let canvas: Canvas;
if (logo) {
const names = await getLogoNames();
if (!names)
throw createError({
statusCode: 500,
message: "Could not retrieve logos",
});
const match = names.find((n) => normalize(n) === normalize(logo));
const resolvedLogo = match ?? logo;
canvas = await renderQRCodeToCanvas(
content,
resolve("public", `logos/${resolvedLogo}.png`),
);
} else {
canvas = await renderQRCodeToCanvas(content, undefined);
}
const image = canvas.toBuffer(format);
event.node.res.setHeader("Content-Type", `image/${format}`);

View File

@@ -1,22 +1,9 @@
import { readdir } from "fs/promises";
import { join } from "path";
import { createHash } from "crypto";
import { getLogoNames } from "../utils/logos";
const logosDir = import.meta.dev
? join(process.cwd(), "public/logos")
: join(process.cwd(), ".output/public/logos");
export default defineEventHandler(async () => {
const names = await getLogoNames();
if (!names)
throw createError({ statusCode: 500, message: "Could not retrieve logos" });
export default defineCachedEventHandler(async () => {
const files = await readdir(logosDir);
return files
.filter((f) => f.endsWith(".png"))
.map((f) => f.replace(/\.png$/, ""))
.sort();
}, {
maxAge: 60 * 60 * 24,
getKey: async () => {
const files = await readdir(logosDir);
const key = files.filter((f) => f.endsWith(".png")).sort().join(",");
return createHash("sha256").update(key).digest("hex");
},
return names;
});

29
server/utils/logos.ts Normal file
View File

@@ -0,0 +1,29 @@
import { readdir } from "fs/promises";
import { join } from "path";
import { createHash } from "crypto";
const logosDir = import.meta.dev
? join(process.cwd(), "public/logos")
: join(process.cwd(), ".output/public/logos");
export const getLogoNames = cachedFunction(
async () => {
const files = await readdir(logosDir);
return files
.filter((f) => f.endsWith(".png"))
.map((f) => f.replace(/\.png$/, ""))
.sort();
},
{
maxAge: 60 * 60 * 24,
name: "logoNames",
getKey: async () => {
const files = await readdir(logosDir);
const key = files
.filter((f) => f.endsWith(".png"))
.sort()
.join(",");
return createHash("sha256").update(key).digest("hex");
},
},
);