WinterCMS research
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
---
|
||||
phase: 15-locale-detection-routing
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["15-01"]
|
||||
files_modified:
|
||||
- themes/quotify/partials/header.htm
|
||||
- themes/quotify/partials/language-switcher.htm
|
||||
- themes/quotify/layouts/default.htm
|
||||
- themes/quotify/layouts/dashboard.htm
|
||||
- themes/quotify/layouts/empty.htm
|
||||
- themes/quotify/assets/css/components/language-switcher.css
|
||||
- themes/quotify/assets/css/app.css
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Language switcher dropdown visible in header"
|
||||
- "Clicking language option switches locale and reloads page"
|
||||
- "Current language name shown in native form (Polski, Deutsch, English)"
|
||||
- "All pages have hreflang tags for SEO"
|
||||
artifacts:
|
||||
- path: "themes/quotify/partials/language-switcher.htm"
|
||||
provides: "Dropdown language switcher UI"
|
||||
contains: "data-request=\"localePicker::onSwitchLocale\""
|
||||
- path: "themes/quotify/partials/header.htm"
|
||||
provides: "Header with language switcher"
|
||||
contains: "language-switcher"
|
||||
- path: "themes/quotify/layouts/default.htm"
|
||||
provides: "Layout with hreflang and LocalePicker"
|
||||
contains: "alternateHrefLangElements"
|
||||
key_links:
|
||||
- from: "themes/quotify/partials/language-switcher.htm"
|
||||
to: "localePicker::onSwitchLocale"
|
||||
via: "data-request attribute"
|
||||
pattern: "data-request.*localePicker::onSwitchLocale"
|
||||
- from: "themes/quotify/layouts/default.htm"
|
||||
to: "AlternateHrefLangElements component"
|
||||
via: "component registration"
|
||||
pattern: "\\[localePicker\\]|\\[alternateHrefLangElements\\]"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create language switcher UI in header and integrate hreflang tags for SEO across all layouts.
|
||||
|
||||
Purpose: Enable users to switch languages via visible dropdown in header, with proper SEO markup for search engines.
|
||||
Output: Language switcher partial, updated header, hreflang integration in all layouts
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/jin/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/jin/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/phases/15-locale-detection-routing/15-CONTEXT.md
|
||||
@.planning/phases/15-locale-detection-routing/15-RESEARCH.md
|
||||
|
||||
@themes/quotify/partials/header.htm
|
||||
@themes/quotify/layouts/default.htm
|
||||
@themes/quotify/layouts/dashboard.htm
|
||||
@themes/quotify/assets/css/app.css
|
||||
@plugins/golem15/translate/components/LocalePicker.php
|
||||
@plugins/golem15/translate/components/AlternateHrefLangElements.php
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Create language switcher partial and CSS</name>
|
||||
<files>
|
||||
themes/quotify/partials/language-switcher.htm
|
||||
themes/quotify/assets/css/components/language-switcher.css
|
||||
themes/quotify/assets/css/app.css
|
||||
</files>
|
||||
<action>
|
||||
Create `themes/quotify/partials/language-switcher.htm`:
|
||||
|
||||
```twig
|
||||
{% set locales = localePicker.locales %}
|
||||
{% set activeLocale = localePicker.activeLocale %}
|
||||
{% set activeLocaleName = localePicker.activeLocaleName %}
|
||||
|
||||
<div class="language-switcher" data-language-switcher>
|
||||
<button type="button" class="language-switcher-toggle" aria-expanded="false" aria-haspopup="listbox" aria-label="{{ 'Select language'|_ }}">
|
||||
<span class="language-switcher-label">{{ activeLocaleName }}</span>
|
||||
<svg class="language-switcher-icon" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</button>
|
||||
<ul class="language-switcher-list" role="listbox" aria-label="{{ 'Available languages'|_ }}">
|
||||
{% for code, name in locales %}
|
||||
<li role="option" {% if code == activeLocale %}aria-selected="true"{% endif %}>
|
||||
<a href="#"
|
||||
class="language-switcher-option {% if code == activeLocale %}active{% endif %}"
|
||||
data-request="localePicker::onSwitchLocale"
|
||||
data-request-data="locale: '{{ code }}'">
|
||||
{{ name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
```
|
||||
|
||||
Create `themes/quotify/assets/css/components/language-switcher.css`:
|
||||
|
||||
```css
|
||||
/* Language Switcher */
|
||||
.language-switcher {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.language-switcher-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-gray-200);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
color: var(--color-gray-700);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.language-switcher-toggle:hover {
|
||||
border-color: var(--color-gray-300);
|
||||
background: var(--color-gray-50);
|
||||
}
|
||||
|
||||
.language-switcher-toggle:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.language-switcher-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
transition: transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.language-switcher.open .language-switcher-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.language-switcher-list {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
z-index: 50;
|
||||
min-width: 140px;
|
||||
margin-top: var(--space-1);
|
||||
padding: var(--space-1);
|
||||
background: var(--color-white);
|
||||
border: 1px solid var(--color-gray-200);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-lg);
|
||||
list-style: none;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-8px);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.language-switcher.open .language-switcher-list {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.language-switcher-option {
|
||||
display: block;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-gray-700);
|
||||
text-decoration: none;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.language-switcher-option:hover {
|
||||
background: var(--color-gray-50);
|
||||
color: var(--color-gray-900);
|
||||
}
|
||||
|
||||
.language-switcher-option.active {
|
||||
background: rgba(0, 102, 102, 0.08);
|
||||
color: var(--color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.language-switcher-option:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* Mobile: Slightly larger touch targets */
|
||||
@media (max-width: 768px) {
|
||||
.language-switcher-toggle {
|
||||
padding: var(--space-2);
|
||||
}
|
||||
|
||||
.language-switcher-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.language-switcher-toggle::before {
|
||||
content: '';
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129'/%3E%3C/svg%3E");
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.language-switcher-option {
|
||||
padding: var(--space-3) var(--space-4);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Add import to `themes/quotify/assets/css/app.css` after other component imports:
|
||||
```css
|
||||
@import 'components/language-switcher.css';
|
||||
```
|
||||
|
||||
Add JavaScript for dropdown toggle to the partial (inline script at bottom):
|
||||
```html
|
||||
<script>
|
||||
(function() {
|
||||
var switcher = document.querySelector('[data-language-switcher]');
|
||||
if (!switcher) return;
|
||||
|
||||
var toggle = switcher.querySelector('.language-switcher-toggle');
|
||||
var list = switcher.querySelector('.language-switcher-list');
|
||||
|
||||
toggle.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
var isOpen = switcher.classList.contains('open');
|
||||
switcher.classList.toggle('open');
|
||||
toggle.setAttribute('aria-expanded', !isOpen);
|
||||
});
|
||||
|
||||
// Close on click outside
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!switcher.contains(e.target)) {
|
||||
switcher.classList.remove('open');
|
||||
toggle.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
});
|
||||
|
||||
// Close on Escape
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && switcher.classList.contains('open')) {
|
||||
switcher.classList.remove('open');
|
||||
toggle.setAttribute('aria-expanded', 'false');
|
||||
toggle.focus();
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
```
|
||||
</action>
|
||||
<verify>Files exist and contain expected content: dropdown HTML, CSS styles, JS toggle logic</verify>
|
||||
<done>Language switcher partial created with accessible dropdown, CSS styling, and toggle JavaScript</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Integrate language switcher into header</name>
|
||||
<files>themes/quotify/partials/header.htm</files>
|
||||
<action>
|
||||
Update `themes/quotify/partials/header.htm` to include the language switcher in the desktop navigation area, near the right side of the header.
|
||||
|
||||
Add the language switcher partial between the nav and the mobile menu toggle:
|
||||
|
||||
```twig
|
||||
<!-- Desktop Navigation -->
|
||||
<nav class="main-nav" aria-label="Main navigation">
|
||||
{% partial 'nav' %}
|
||||
</nav>
|
||||
|
||||
<!-- Language Switcher -->
|
||||
<div class="header-actions">
|
||||
{% partial 'language-switcher' %}
|
||||
</div>
|
||||
|
||||
<!-- Mobile Menu Toggle -->
|
||||
```
|
||||
|
||||
Add CSS for header-actions positioning (add to header.htm inline styles or ensure it exists in header CSS):
|
||||
|
||||
The header-actions div should be positioned to the right, before the mobile menu toggle. If using flexbox on header-inner, it will naturally flow to the right.
|
||||
|
||||
Style addition (can be inline or in existing header CSS):
|
||||
```css
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header-actions {
|
||||
order: 2; /* After logo, before menu toggle */
|
||||
}
|
||||
}
|
||||
```
|
||||
</action>
|
||||
<verify>Header partial includes language-switcher partial with header-actions wrapper</verify>
|
||||
<done>Language switcher integrated into site header, visible on all pages</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Register components and add hreflang to all layouts</name>
|
||||
<files>
|
||||
themes/quotify/layouts/default.htm
|
||||
themes/quotify/layouts/dashboard.htm
|
||||
themes/quotify/layouts/empty.htm
|
||||
</files>
|
||||
<action>
|
||||
Update all three layouts to:
|
||||
1. Register localePicker and alternateHrefLangElements components
|
||||
2. Add hreflang output in head section
|
||||
|
||||
For each layout, add component registration in the configuration section:
|
||||
|
||||
```ini
|
||||
[localePicker]
|
||||
|
||||
[alternateHrefLangElements]
|
||||
```
|
||||
|
||||
In the `<head>` section, after the meta tags and before stylesheets, add:
|
||||
|
||||
```twig
|
||||
<!-- Alternate language URLs for SEO -->
|
||||
{% component 'alternateHrefLangElements' %}
|
||||
```
|
||||
|
||||
This generates:
|
||||
```html
|
||||
<link rel="alternate" hreflang="en" href="https://quotify.pro/current-page">
|
||||
<link rel="alternate" hreflang="pl" href="https://quotify.pro/pl/current-page">
|
||||
<link rel="alternate" hreflang="de" href="https://quotify.pro/de/current-page">
|
||||
```
|
||||
|
||||
For default.htm - add both components
|
||||
For dashboard.htm - add both components
|
||||
For empty.htm - check if it exists, add both components if present
|
||||
|
||||
The localePicker component is needed so the language-switcher partial can access its properties (locales, activeLocale, etc.).
|
||||
</action>
|
||||
<verify>All layouts register localePicker and alternateHrefLangElements components, and output hreflang in head</verify>
|
||||
<done>All layouts have LocalePicker for switcher functionality and hreflang tags for SEO</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. Visual: Language switcher dropdown visible in header on all pages
|
||||
2. Functional: Click switcher, dropdown opens with language options
|
||||
3. Functional: Click a language option, page reloads in that language
|
||||
4. Accessibility: Switcher focusable, Escape closes dropdown
|
||||
5. SEO: View page source, confirm hreflang tags present for en, pl, de
|
||||
6. Mobile: On mobile viewport, switcher shows icon only (label hidden)
|
||||
7. Styling: Matches design palette (teal primary, proper spacing)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Language switcher partial exists with dropdown UI
|
||||
- Header displays language switcher near right side
|
||||
- Dropdown shows all enabled locales (English, Polski, Deutsch)
|
||||
- Clicking language triggers localePicker::onSwitchLocale
|
||||
- All layouts register localePicker and alternateHrefLangElements components
|
||||
- hreflang tags present in page source for all enabled locales
|
||||
- Responsive design: icon-only on mobile
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/15-locale-detection-routing/15-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user