feat(ui): add chip-based keyword input for pages SEO
Replace plain text input with interactive chip UI for SEO keywords in repository pages settings. Users can add keywords individually with validation, and remove them by clicking the × button. Backend now trims whitespace from keywords. Improves UX for managing keyword lists.
This commit is contained in:
@@ -725,6 +725,8 @@
|
||||
"repo.settings.pages.seo_title": "SEO Title",
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.seo_keywords.placeholder": "Add a keyword...",
|
||||
"repo.settings.pages.seo_keywords.add": "Add",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.vault.plugin_not_installed": "Vault Plugin Not Installed",
|
||||
"repo.vault.plugin_not_installed_desc": "The Vault plugin is not installed on this server. Contact your administrator to enable secrets management.",
|
||||
|
||||
@@ -4493,6 +4493,8 @@
|
||||
"repo.settings.pages.seo_title": "SEO Title",
|
||||
"repo.settings.pages.seo_description": "Meta Description",
|
||||
"repo.settings.pages.seo_keywords": "Keywords",
|
||||
"repo.settings.pages.seo_keywords.placeholder": "Add a keyword...",
|
||||
"repo.settings.pages.seo_keywords.add": "Add",
|
||||
"repo.settings.pages.og_image": "Open Graph Image URL",
|
||||
"repo.settings.pages.brand_favicon_url": "Favicon URL",
|
||||
"repo.settings.pages.brand_favicon_url_help": "URL to a custom favicon for your landing page (ICO, PNG, or SVG). Leave blank to use the default.",
|
||||
|
||||
@@ -479,7 +479,13 @@ func PagesThemePost(ctx *context.Context) {
|
||||
config.SEO.Description = ctx.FormString("seo_description")
|
||||
keywords := ctx.FormString("seo_keywords")
|
||||
if keywords != "" {
|
||||
config.SEO.Keywords = strings.Split(keywords, ",")
|
||||
parts := strings.Split(keywords, ",")
|
||||
config.SEO.Keywords = make([]string, 0, len(parts))
|
||||
for _, kw := range parts {
|
||||
if trimmed := strings.TrimSpace(kw); trimmed != "" {
|
||||
config.SEO.Keywords = append(config.SEO.Keywords, trimmed)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
config.SEO.Keywords = nil
|
||||
}
|
||||
|
||||
@@ -73,7 +73,15 @@
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pages.seo_keywords"}}</label>
|
||||
<input name="seo_keywords" value="{{.Config.SEO.Keywords}}" placeholder="keyword1, keyword2, keyword3">
|
||||
<input type="hidden" name="seo_keywords" id="seo-keywords-hidden"
|
||||
value="{{StringUtils.Join .Config.SEO.Keywords ","}}">
|
||||
<div id="seo-keywords-chips" style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px;"></div>
|
||||
<div class="tw-flex tw-gap-2">
|
||||
<input type="text" id="seo-keyword-input"
|
||||
placeholder="{{ctx.Locale.Tr "repo.settings.pages.seo_keywords.placeholder"}}"
|
||||
class="tw-flex-1" style="font-size:13px;" maxlength="50">
|
||||
<button class="ui small button" type="button" id="seo-keyword-add">{{ctx.Locale.Tr "repo.settings.pages.seo_keywords.add"}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ctx.Locale.Tr "repo.settings.pages.og_image"}}</label>
|
||||
@@ -87,6 +95,64 @@
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// === Keyword chips ===
|
||||
(function() {
|
||||
const hiddenInput = document.getElementById('seo-keywords-hidden');
|
||||
const chipsContainer = document.getElementById('seo-keywords-chips');
|
||||
const kwInput = document.getElementById('seo-keyword-input');
|
||||
const addBtn = document.getElementById('seo-keyword-add');
|
||||
|
||||
function getKeywords() {
|
||||
const val = hiddenInput.value.trim();
|
||||
if (!val) return [];
|
||||
return val.split(',').map(t => t.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
function setKeywords(keywords) {
|
||||
hiddenInput.value = keywords.join(',');
|
||||
}
|
||||
|
||||
function renderChips() {
|
||||
chipsContainer.innerHTML = '';
|
||||
getKeywords().forEach(function(kw) {
|
||||
const chip = document.createElement('span');
|
||||
chip.className = 'ui small label';
|
||||
chip.style.cssText = 'display:inline-flex;align-items:center;gap:4px;margin:0;';
|
||||
chip.textContent = kw;
|
||||
const x = document.createElement('button');
|
||||
x.type = 'button';
|
||||
x.style.cssText = 'background:none;border:none;cursor:pointer;padding:0;margin:0;font-size:14px;line-height:1;color:inherit;opacity:0.7;';
|
||||
x.innerHTML = '×';
|
||||
x.addEventListener('click', function() {
|
||||
setKeywords(getKeywords().filter(t => t !== kw));
|
||||
renderChips();
|
||||
});
|
||||
chip.appendChild(x);
|
||||
chipsContainer.appendChild(chip);
|
||||
});
|
||||
}
|
||||
|
||||
function addKeyword() {
|
||||
const val = kwInput.value.trim();
|
||||
if (!val) return;
|
||||
const keywords = getKeywords();
|
||||
if (!keywords.includes(val)) {
|
||||
keywords.push(val);
|
||||
setKeywords(keywords);
|
||||
renderChips();
|
||||
}
|
||||
kwInput.value = '';
|
||||
kwInput.focus();
|
||||
}
|
||||
|
||||
addBtn.addEventListener('click', addKeyword);
|
||||
kwInput.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') { e.preventDefault(); addKeyword(); }
|
||||
});
|
||||
|
||||
renderChips();
|
||||
})();
|
||||
|
||||
function updateColorPreview(type) {
|
||||
const colorInput = document.getElementById(type + '_color');
|
||||
const preview = document.getElementById(type + '_preview');
|
||||
|
||||
Reference in New Issue
Block a user