diff --git a/docs/docs.go b/docs/docs.go index 2e90a2c..e8e7196 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -540,7 +540,7 @@ const docTemplate = `{ }, "/nav-links": { "get": { - "description": "Public header navigation links (status enabled), ordered by sort ASC then id ASC, with translations.", + "description": "Public site navigation links (header and footer, status enabled), ordered by sort ASC then id ASC, with translations and position.", "produces": [ "application/json" ], @@ -572,7 +572,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Admin only. Create a header navigation link. url accepts site-relative paths (/...), http(s) URLs, and mailto links; translations must include at least one non-empty label.", + "description": "Admin only. Create a site navigation link (position: header or footer, defaults to header). url accepts site-relative paths (/...), http(s) URLs, and mailto links; translations must include at least one non-empty label.", "consumes": [ "application/json" ], @@ -635,7 +635,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Admin only. List all header navigation links including disabled ones, with translations.", + "description": "Admin only. List all site navigation links including disabled ones, with translations.", "produces": [ "application/json" ], @@ -681,7 +681,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Admin only. Update a header navigation link; translations are replaced by the provided list.", + "description": "Admin only. Update a site navigation link; position is kept when omitted; translations are replaced by the provided list.", "consumes": [ "application/json" ], @@ -756,7 +756,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Admin only. Delete a header navigation link and its translations.", + "description": "Admin only. Delete a site navigation link and its translations.", "produces": [ "application/json" ], @@ -1133,7 +1133,7 @@ const docTemplate = `{ }, "/site": { "get": { - "description": "Public site settings: site name, logo URL, and footer text. Returns built-in defaults when the settings row is missing.", + "description": "Public site settings: site name, logo URL, favicon URL, and footer text. Returns built-in defaults when the settings row is missing.", "produces": [ "application/json" ], @@ -1162,7 +1162,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Admin only. Update the site name, logo URL, and footer text; logo files hosted on this site have their reference count managed automatically. Returns the updated settings.", + "description": "Admin only. Update the site name, logo URL, favicon URL, and footer text; logo and favicon files hosted on this site have their reference count managed automatically. Returns the updated settings.", "consumes": [ "application/json" ], @@ -1192,7 +1192,7 @@ const docTemplate = `{ } }, "400": { - "description": "invalid request or logo file not found", + "description": "invalid request or file not found", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -1218,6 +1218,114 @@ const docTemplate = `{ } } }, + "/site/favicon": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Admin only. Upload an image (raster or SVG) as the site favicon (multipart field file); takes effect immediately and file references are managed automatically. SVG is served inline with a restrictive Content-Security-Policy.", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Upload site favicon", + "parameters": [ + { + "type": "file", + "description": "Favicon image", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/model.SiteSetting" + } + }, + "400": { + "description": "invalid request, empty file, or not an image", + "schema": { + "$ref": "#/definitions/httpx.ErrorResponse" + } + }, + "401": { + "description": "unauthorized or session expired", + "schema": { + "$ref": "#/definitions/httpx.ErrorResponse" + } + }, + "403": { + "description": "admin permission required or account disabled", + "schema": { + "$ref": "#/definitions/httpx.ErrorResponse" + } + }, + "413": { + "description": "file too large", + "schema": { + "$ref": "#/definitions/httpx.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/httpx.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Admin only. Clear the site favicon and release the reference of the locally stored favicon file.", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Delete site favicon", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/model.SiteSetting" + } + }, + "401": { + "description": "unauthorized or session expired", + "schema": { + "$ref": "#/definitions/httpx.ErrorResponse" + } + }, + "403": { + "description": "admin permission required or account disabled", + "schema": { + "$ref": "#/definitions/httpx.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/httpx.ErrorResponse" + } + } + } + } + }, "/site/logo": { "put": { "security": [ @@ -1225,7 +1333,7 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Admin only. Upload an image as the site logo (multipart field file, image only); the logo takes effect immediately and file references are managed automatically.", + "description": "Admin only. Upload a raster image as the site logo (multipart field file); the logo takes effect immediately and file references are managed automatically.", "consumes": [ "multipart/form-data" ], @@ -2154,6 +2262,9 @@ const docTemplate = `{ "open_in_new_window": { "type": "boolean" }, + "position": { + "type": "string" + }, "sort": { "type": "integer" }, @@ -2211,6 +2322,10 @@ const docTemplate = `{ "model.SiteSetting": { "type": "object", "properties": { + "favicon": { + "type": "string", + "example": "https://example.com/favicon.ico" + }, "footer": { "type": "string", "example": "Copyright © Rill" @@ -2305,6 +2420,14 @@ const docTemplate = `{ "type": "boolean", "example": false }, + "position": { + "type": "string", + "enum": [ + "header", + "footer" + ], + "example": "header" + }, "sort": { "type": "integer", "example": 0 @@ -2396,6 +2519,11 @@ const docTemplate = `{ "site_name" ], "properties": { + "favicon": { + "type": "string", + "maxLength": 500, + "example": "https://example.com/favicon.ico" + }, "footer": { "type": "string", "maxLength": 1000, diff --git a/docs/swagger.json b/docs/swagger.json index ba2265c..4573e69 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -533,7 +533,7 @@ }, "/nav-links": { "get": { - "description": "Public header navigation links (status enabled), ordered by sort ASC then id ASC, with translations.", + "description": "Public site navigation links (header and footer, status enabled), ordered by sort ASC then id ASC, with translations and position.", "produces": [ "application/json" ], @@ -565,7 +565,7 @@ "BearerAuth": [] } ], - "description": "Admin only. Create a header navigation link. url accepts site-relative paths (/...), http(s) URLs, and mailto links; translations must include at least one non-empty label.", + "description": "Admin only. Create a site navigation link (position: header or footer, defaults to header). url accepts site-relative paths (/...), http(s) URLs, and mailto links; translations must include at least one non-empty label.", "consumes": [ "application/json" ], @@ -628,7 +628,7 @@ "BearerAuth": [] } ], - "description": "Admin only. List all header navigation links including disabled ones, with translations.", + "description": "Admin only. List all site navigation links including disabled ones, with translations.", "produces": [ "application/json" ], @@ -674,7 +674,7 @@ "BearerAuth": [] } ], - "description": "Admin only. Update a header navigation link; translations are replaced by the provided list.", + "description": "Admin only. Update a site navigation link; position is kept when omitted; translations are replaced by the provided list.", "consumes": [ "application/json" ], @@ -749,7 +749,7 @@ "BearerAuth": [] } ], - "description": "Admin only. Delete a header navigation link and its translations.", + "description": "Admin only. Delete a site navigation link and its translations.", "produces": [ "application/json" ], @@ -1126,7 +1126,7 @@ }, "/site": { "get": { - "description": "Public site settings: site name, logo URL, and footer text. Returns built-in defaults when the settings row is missing.", + "description": "Public site settings: site name, logo URL, favicon URL, and footer text. Returns built-in defaults when the settings row is missing.", "produces": [ "application/json" ], @@ -1155,7 +1155,7 @@ "BearerAuth": [] } ], - "description": "Admin only. Update the site name, logo URL, and footer text; logo files hosted on this site have their reference count managed automatically. Returns the updated settings.", + "description": "Admin only. Update the site name, logo URL, favicon URL, and footer text; logo and favicon files hosted on this site have their reference count managed automatically. Returns the updated settings.", "consumes": [ "application/json" ], @@ -1185,7 +1185,7 @@ } }, "400": { - "description": "invalid request or logo file not found", + "description": "invalid request or file not found", "schema": { "$ref": "#/definitions/httpx.ErrorResponse" } @@ -1211,6 +1211,114 @@ } } }, + "/site/favicon": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Admin only. Upload an image (raster or SVG) as the site favicon (multipart field file); takes effect immediately and file references are managed automatically. SVG is served inline with a restrictive Content-Security-Policy.", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Upload site favicon", + "parameters": [ + { + "type": "file", + "description": "Favicon image", + "name": "file", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/model.SiteSetting" + } + }, + "400": { + "description": "invalid request, empty file, or not an image", + "schema": { + "$ref": "#/definitions/httpx.ErrorResponse" + } + }, + "401": { + "description": "unauthorized or session expired", + "schema": { + "$ref": "#/definitions/httpx.ErrorResponse" + } + }, + "403": { + "description": "admin permission required or account disabled", + "schema": { + "$ref": "#/definitions/httpx.ErrorResponse" + } + }, + "413": { + "description": "file too large", + "schema": { + "$ref": "#/definitions/httpx.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/httpx.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Admin only. Clear the site favicon and release the reference of the locally stored favicon file.", + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Delete site favicon", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/model.SiteSetting" + } + }, + "401": { + "description": "unauthorized or session expired", + "schema": { + "$ref": "#/definitions/httpx.ErrorResponse" + } + }, + "403": { + "description": "admin permission required or account disabled", + "schema": { + "$ref": "#/definitions/httpx.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/httpx.ErrorResponse" + } + } + } + } + }, "/site/logo": { "put": { "security": [ @@ -1218,7 +1326,7 @@ "BearerAuth": [] } ], - "description": "Admin only. Upload an image as the site logo (multipart field file, image only); the logo takes effect immediately and file references are managed automatically.", + "description": "Admin only. Upload a raster image as the site logo (multipart field file); the logo takes effect immediately and file references are managed automatically.", "consumes": [ "multipart/form-data" ], @@ -2147,6 +2255,9 @@ "open_in_new_window": { "type": "boolean" }, + "position": { + "type": "string" + }, "sort": { "type": "integer" }, @@ -2204,6 +2315,10 @@ "model.SiteSetting": { "type": "object", "properties": { + "favicon": { + "type": "string", + "example": "https://example.com/favicon.ico" + }, "footer": { "type": "string", "example": "Copyright © Rill" @@ -2298,6 +2413,14 @@ "type": "boolean", "example": false }, + "position": { + "type": "string", + "enum": [ + "header", + "footer" + ], + "example": "header" + }, "sort": { "type": "integer", "example": 0 @@ -2389,6 +2512,11 @@ "site_name" ], "properties": { + "favicon": { + "type": "string", + "maxLength": 500, + "example": "https://example.com/favicon.ico" + }, "footer": { "type": "string", "maxLength": 1000, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 1f2263c..1cdd60f 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -113,6 +113,8 @@ definitions: type: integer open_in_new_window: type: boolean + position: + type: string sort: type: integer status: @@ -150,6 +152,9 @@ definitions: type: object model.SiteSetting: properties: + favicon: + example: https://example.com/favicon.ico + type: string footer: example: Copyright © Rill type: string @@ -211,6 +216,12 @@ definitions: open_in_new_window: example: false type: boolean + position: + enum: + - header + - footer + example: header + type: string sort: example: 0 type: integer @@ -277,6 +288,10 @@ definitions: type: object site.UpdateRequest: properties: + favicon: + example: https://example.com/favicon.ico + maxLength: 500 + type: string footer: example: Copyright © Rill maxLength: 1000 @@ -788,8 +803,8 @@ paths: - user /nav-links: get: - description: Public header navigation links (status enabled), ordered by sort - ASC then id ASC, with translations. + description: Public site navigation links (header and footer, status enabled), + ordered by sort ASC then id ASC, with translations and position. produces: - application/json responses: @@ -809,9 +824,9 @@ paths: post: consumes: - application/json - description: Admin only. Create a header navigation link. url accepts site-relative - paths (/...), http(s) URLs, and mailto links; translations must include at - least one non-empty label. + description: 'Admin only. Create a site navigation link (position: header or + footer, defaults to header). url accepts site-relative paths (/...), http(s) + URLs, and mailto links; translations must include at least one non-empty label.' parameters: - description: Nav link payload in: body @@ -849,7 +864,7 @@ paths: - admin /nav-links/{id}: delete: - description: Admin only. Delete a header navigation link and its translations. + description: Admin only. Delete a site navigation link and its translations. parameters: - description: Nav link ID example: 1 @@ -890,8 +905,8 @@ paths: put: consumes: - application/json - description: Admin only. Update a header navigation link; translations are replaced - by the provided list. + description: Admin only. Update a site navigation link; position is kept when + omitted; translations are replaced by the provided list. parameters: - description: Nav link ID example: 1 @@ -939,8 +954,8 @@ paths: - admin /nav-links/list: get: - description: Admin only. List all header navigation links including disabled - ones, with translations. + description: Admin only. List all site navigation links including disabled ones, + with translations. produces: - application/json responses: @@ -1177,8 +1192,8 @@ paths: - user /site: get: - description: 'Public site settings: site name, logo URL, and footer text. Returns - built-in defaults when the settings row is missing.' + description: 'Public site settings: site name, logo URL, favicon URL, and footer + text. Returns built-in defaults when the settings row is missing.' produces: - application/json responses: @@ -1196,9 +1211,9 @@ paths: put: consumes: - application/json - description: Admin only. Update the site name, logo URL, and footer text; logo - files hosted on this site have their reference count managed automatically. - Returns the updated settings. + description: Admin only. Update the site name, logo URL, favicon URL, and footer + text; logo and favicon files hosted on this site have their reference count + managed automatically. Returns the updated settings. parameters: - description: Site settings in: body @@ -1214,7 +1229,7 @@ paths: schema: $ref: '#/definitions/model.SiteSetting' "400": - description: invalid request or logo file not found + description: invalid request or file not found schema: $ref: '#/definitions/httpx.ErrorResponse' "401": @@ -1234,6 +1249,78 @@ paths: summary: Update site settings tags: - admin + /site/favicon: + delete: + description: Admin only. Clear the site favicon and release the reference of + the locally stored favicon file. + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/model.SiteSetting' + "401": + description: unauthorized or session expired + schema: + $ref: '#/definitions/httpx.ErrorResponse' + "403": + description: admin permission required or account disabled + schema: + $ref: '#/definitions/httpx.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/httpx.ErrorResponse' + security: + - BearerAuth: [] + summary: Delete site favicon + tags: + - admin + put: + consumes: + - multipart/form-data + description: Admin only. Upload an image (raster or SVG) as the site favicon + (multipart field file); takes effect immediately and file references are managed + automatically. SVG is served inline with a restrictive Content-Security-Policy. + parameters: + - description: Favicon image + in: formData + name: file + required: true + type: file + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/model.SiteSetting' + "400": + description: invalid request, empty file, or not an image + schema: + $ref: '#/definitions/httpx.ErrorResponse' + "401": + description: unauthorized or session expired + schema: + $ref: '#/definitions/httpx.ErrorResponse' + "403": + description: admin permission required or account disabled + schema: + $ref: '#/definitions/httpx.ErrorResponse' + "413": + description: file too large + schema: + $ref: '#/definitions/httpx.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/httpx.ErrorResponse' + security: + - BearerAuth: [] + summary: Upload site favicon + tags: + - admin /site/logo: delete: description: Admin only. Clear the site logo and release the reference of the @@ -1265,9 +1352,8 @@ paths: put: consumes: - multipart/form-data - description: Admin only. Upload an image as the site logo (multipart field file, - image only); the logo takes effect immediately and file references are managed - automatically. + description: Admin only. Upload a raster image as the site logo (multipart field + file); the logo takes effect immediately and file references are managed automatically. parameters: - description: Logo image in: formData diff --git a/frontend/src/App.vue b/frontend/src/App.vue index dd28865..d04d481 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -15,6 +15,20 @@ watch( }, { immediate: true }, ) + +watch( + () => site.favicon, + (favicon) => { + let link = document.querySelector('link[rel="icon"]') + if (!link) { + link = document.createElement('link') + link.rel = 'icon' + document.head.appendChild(link) + } + link.href = favicon || '/favicon.ico' + }, + { immediate: true }, +)