diff --git a/.gitignore b/.gitignore
index fce1a429..9f1276da 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
-node_modules/
+# node_modules as a folder or a link (links were once committed by mistake)
+node_modules
dist/
.astro/
.env
diff --git a/api/node_modules b/api/node_modules
deleted file mode 120000
index 1ee83cb3..00000000
--- a/api/node_modules
+++ /dev/null
@@ -1 +0,0 @@
-/Users/anasrashid/divan/api/node_modules
\ No newline at end of file
diff --git a/api/src/server.ts b/api/src/server.ts
index 29155658..445742c9 100644
--- a/api/src/server.ts
+++ b/api/src/server.ts
@@ -25,6 +25,25 @@ import { ebookRoutes } from './ebooks.ts';
import { feedRoutes } from './feeds.ts';
const app = Fastify({ logger: { level: process.env.LOG_LEVEL ?? 'info' } });
+
+// Errors: readers get a short message, never internal details; the log gets the whole error (one JSON line each,
+// with the method and address), which is what the server's error report reads (divan-deploy scripts/errors.sh).
+app.setErrorHandler((err: any, req, reply) => {
+ const status = err.statusCode >= 400 && err.statusCode < 500 ? err.statusCode : 500;
+ if (status === 500) req.log.error({ err, method: req.method, url: req.url }, 'request failed');
+ reply.code(status).send({ error: status === 500 ? 'سرور میں خرابی۔ تھوڑی دیر بعد کوشش کریں۔' : err.message });
+});
+
+// for the site's /health (uptime monitoring): the API answers and the database too
+app.get('/api/health', async (req, reply) => {
+ try {
+ await pool.query('SELECT 1');
+ return { ok: true, db: 'ok' };
+ } catch (err) {
+ req.log.error({ err }, 'health: database unreachable');
+ return reply.code(503).send({ ok: false, db: 'down' });
+ }
+});
const PAGE_SIZE = 20;
app.get('/health', async () => {
diff --git a/web/node_modules b/web/node_modules
deleted file mode 120000
index 0fd07bfe..00000000
--- a/web/node_modules
+++ /dev/null
@@ -1 +0,0 @@
-/Users/anasrashid/divan/web/node_modules
\ No newline at end of file
diff --git a/web/src/components/Icon.astro b/web/src/components/Icon.astro
index 3a2915ef..d950292e 100644
--- a/web/src/components/Icon.astro
+++ b/web/src/components/Icon.astro
@@ -56,6 +56,9 @@ const PATHS = {
chevron: '',
// footer
rss: '',
+ home: '',
+ lock: '',
+ scale: '',
info: '',
globe: '',
database: '',
diff --git a/web/src/layouts/Base.astro b/web/src/layouts/Base.astro
index b6bb27a0..f20dba90 100644
--- a/web/src/layouts/Base.astro
+++ b/web/src/layouts/Base.astro
@@ -136,6 +136,8 @@ import { brand } from '../lib/brand';
{brand.name} کے بارے میںمقصد اور طریقۂ کار
ٹیگموضوعات سے کلام
نئی تبدیلیاںRSS: نئے ورژن اور ای بکس
+ رازداریہم کیا محفوظ رکھتے ہیں
+ استعمال کی شرائطلائسنس اور ذمہ داریاں
ماخذ
- ویکی ماخذمتن · عوامی ملکیت
diff --git a/web/src/lib/brand.ts b/web/src/lib/brand.ts
index 38c207ae..18a98abd 100644
--- a/web/src/lib/brand.ts
+++ b/web/src/lib/brand.ts
@@ -4,6 +4,8 @@ export const brand = {
name: 'دیوان',
tagline: 'اردو کا کلاسیکی ادب',
madeBy: 'ایک پاکستانی کی طرف سے، پاکستان کے لیے', // footer, beside Pakistan's flag
+ // where readers write: questions, corrections, copyright claims (privacy and terms pages)
+ contact: { text: 'ہم سے رابطہ کریں', url: 'https://git.anasrashid.net/anas/divan/issues' },
// the attribution forks keep (NOTICE, GPL v3 section 7(b))
basedOn: { text: 'Divan by Anas Rashid', url: 'https://git.anasrashid.net/anas/divan' },
};
diff --git a/web/src/middleware.ts b/web/src/middleware.ts
index d0d91be4..a24b0bad 100644
--- a/web/src/middleware.ts
+++ b/web/src/middleware.ts
@@ -1,6 +1,6 @@
// The signed-in reader (or null) for every page, from the session cookie; and the reader's numerals: pages are
// written with Eastern Arabic (Urdu) digits, and a reader who chose Western digits (cookie divan-digits=latn, set in
-// the display settings) gets them in the page as sent, so nothing flashes
+// the display settings) gets them in the page as sent, so nothing flashes. Errors are logged as one JSON line each.
import { defineMiddleware } from 'astro:middleware';
import { auth, COOKIE } from './lib/auth';
@@ -12,7 +12,15 @@ export const onRequest = defineMiddleware(async (ctx, next) => {
if (r.ok) ctx.locals.user = r.data.user;
else if (r.status === 401) ctx.cookies.delete(COOKIE, { path: '/' }); // expired or signed out elsewhere
}
- const res = await next();
+ let res: Response;
+ try {
+ res = await next();
+ } catch (err: any) {
+ // one JSON line per error (the server's error report reads these); the reader sees the Urdu error page (500.astro)
+ console.error(JSON.stringify({ level: 'error', time: new Date().toISOString(), method: ctx.request.method, path: ctx.url.pathname,
+ msg: String(err?.message ?? err), stack: String(err?.stack ?? '').split('\n').slice(0, 8).join('\n') }));
+ throw err;
+ }
if (ctx.cookies.get('divan-digits')?.value !== 'latn' || !res.headers.get('content-type')?.includes('text/html')) return res;
// text only: scripts and styles are left alone (the page's own scripts look for Eastern digits)
const html = (await res.text()).split(/(