diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index a48b55d..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(git add:*)", - "Bash(git commit:*)", - "Bash(cat:*)" - ], - "deny": [], - "ask": [] - } -} diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index 67cc286..6203168 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -32,7 +32,7 @@ jobs: set ftp:ssl-allow no open -u $USERNAME,$PASSWORD $PREPRODUCTION_HOST mirror --reverse --verbose --ignore-time --parallel=10 -x local/ assets assets - mirror --reverse --verbose --ignore-time --parallel=10 -x accounts/ -x cache/ -x sessions/ -x header.php site site + mirror --reverse --verbose --ignore-time --parallel=10 -x accounts/ -x cache/ -x sessions/ site site mirror --reverse --verbose --ignore-time --parallel=10 kirby kirby mirror --reverse --verbose --ignore-time --parallel=10 vendor vendor quit diff --git a/.gitignore b/.gitignore index 42cbd09..510df57 100644 --- a/.gitignore +++ b/.gitignore @@ -61,4 +61,5 @@ Icon # Local local/ -/local/* \ No newline at end of file +.claude +.claude/* diff --git a/assets/css/base/_body.scss b/assets/css/base/_body.scss index b15509b..4bfe9c1 100644 --- a/assets/css/base/_body.scss +++ b/assets/css/base/_body.scss @@ -29,9 +29,6 @@ body{ color: var(--color-txt); background-color: var(--color-bg); - width: 100vw; - overflow-x: hidden; - } img{ @@ -51,7 +48,7 @@ img{ // } } - +.swiper-button-next, .swiper-button-prev, body, #site-header, #site-footer{ transition: background-color 0.3s ease, color 0.3s ease; } @@ -61,4 +58,74 @@ body, #site-header, #site-footer{ grid-gap: var(--padding-inner); grid-template-columns: 2fr 3fr; position: relative; -} \ No newline at end of file +} + + +@mixin hide-scroll(){ + scrollbar-width: none; + -ms-overflow-style: none; + &::-webkit-scrollbar { + display: none; + } +} + +@mixin clamp($lines) { + display: -webkit-box; + -webkit-line-clamp: $lines; + -webkit-box-orient: vertical; + overflow: hidden; +} + +@mixin hover-card-line(){ + border-bottom: var(--border-light); + &:first-of-type{ + border-top: var(--border-light); + } + position: relative; + &::before{ + content: ''; + width: 100%; + border-top: 1px solid transparent; + position: absolute; + top: -1px; + left: 0; + } + + &:hover{ + background-color: var(--grey-950); + border-color: var(--color-txt); + &::before{ + border-color: var(--color-txt); + } + + } +} + + +@mixin pin(){ + .pin{ + z-index: 90; + width: 18px; + height: 18px; + transform: rotate(45deg); + transform-origin: center; + svg{ + width: 100%; + height: 100%; + fill: var(--color-txt); + } + } +} + + + +body.menu-open, +body.is-hidden{ + overflow-y: hidden; + main, footer, #nav-highlight, .btn--back-to-top{ + transition: opacity .3s ease-in; + opacity: 0.1; + } +} + + diff --git a/assets/css/base/_responsive.scss b/assets/css/base/_responsive.scss index 0017258..81a0ffc 100644 --- a/assets/css/base/_responsive.scss +++ b/assets/css/base/_responsive.scss @@ -11,3 +11,8 @@ $paysage: "screen and (max-height: 670px) and (min-width: 1080px)"; @media #{$medium}{ } + + +@media #{$x-small}{ + +} diff --git a/assets/css/base/_var.scss b/assets/css/base/_var.scss index 675f0e1..8edffe3 100644 --- a/assets/css/base/_var.scss +++ b/assets/css/base/_var.scss @@ -2,23 +2,19 @@ --font: 'Executive', Arial, sans-serif; --title: 'System', Arial, sans-serif; - // --fs-xsmall: 10px; - // --fs-small: 12px; - // --fs-normal: 16px; - // --fs-medium: 22px; - // --fs-big: 38px; - --fs-xsmall: 10px; - --fs-small: 12px; - --fs-normal: 16px; - --fs-medium: 20px; - --fs-big: 30px; - --fs-xbig: 38px; + --fs-xsmall: 12px; + --fs-small: 16px; + --fs-normal: 20px; + --fs-medium: 30px; + --fs-big: 45px; --fs-button-bold: 22px; - --max-w-content: 700px; - --max-w-container: 1280px; + --max-w-content: 640px; + --max-w-cards: 940px; + --z-header: 2000; + --panel-w: 310px; @media #{$small} { --fs-medium: 20px; @@ -26,7 +22,9 @@ } --leading-tight: 1.05; - --leading-normal: 1.2; + --leading-normal: 1.3; + + --leading-title: 1.1; // --leading-relaxed: 1.4; // --leading-loose: 1.8; @@ -41,6 +39,7 @@ --grey-400: #969696; --grey-600: #6d6d6d; --grey-800: #383838; + --grey-950: #222222; --color-bg: #161616; --color-txt: #ffffff; @@ -54,7 +53,7 @@ --border: 1px solid var(--color-txt); --border-medium: 1px solid var(--grey-600); - --border-light: 1px solid var(--grey-800); + --border-light: 1px solid #414141; --header-h: 80px; @@ -80,30 +79,31 @@ @media #{$small}{ :root { - --fs-xsmall: 10px; - --fs-small: 12px; - --fs-normal: 14px; - --fs-medium: 18px; - --fs-big: 28px; - --fs-xbig: 32px; + --fs-xsmall: 13px; + --fs-small: 16px; + --fs-normal: 20px; + --fs-medium: 24px; + --fs-big: 34px; --header-h: 60px; + --padding-body: 16px; } } :root[data-theme="light"] { - --grey-100: #1f1f1f; + --grey-100: #2f2f2f; --grey-200: #2f2f2f; --grey-300: #4a4a4a; --grey-400: #6a6a6a; --grey-600: #9a9a9a; --grey-800: #cfcfcf; + --grey-950: #eaeaea; --color-bg: #efefef; - --color-txt: #000000; + --color-txt: #161616; --color-txt-light: var(--grey-400); --color-accent: #ff00ff; diff --git a/assets/css/components/_btn--don.scss b/assets/css/components/_btn--don.scss deleted file mode 100644 index 3c4e2d0..0000000 --- a/assets/css/components/_btn--don.scss +++ /dev/null @@ -1,60 +0,0 @@ -#btn--don__mobile { - width: 100%; - display: flex; - align-items: center; - justify-content: center; - - padding-top: calc(var(--spacing) * 0.5); - padding-bottom: calc(var(--spacing) * 1.5); - position: fixed; - bottom: 0px; - left: 0; - z-index: 100; - opacity: 0; - transition: opacity ease-in 0.2s; - - pointer-events: none; - &.is-visible { - pointer-events: all; - opacity: 1; - } - - &.is-sticky { - position: relative; - } - - @media #{$small-up} { - display: none !important; - } -} - -.btn--don { - --vertical-padding: 0.5ch; - height: calc(var(--h-block) + var(--vertical-padding)); - border-radius: calc(var(--h-block) / 1); - padding: var(--vertical-padding) 2ch; - background-color: var(--color-accent); - color: var(--color-bg); - font-family: var(--font); - font-size: var(--fs-medium); - font-weight: var(--fw-bold); - - a { - text-decoration: none; - display: flex; - align-items: center; - gap: 0.5ch; - } - - .icon { - height: 28px; - position: relative; - top: 1px; - } - - svg { - fill: var(--color-bg); - width: 24px; - height: 24px; - } -} diff --git a/assets/css/components/_btn-group-mobile.scss b/assets/css/components/_btn-group-mobile.scss new file mode 100644 index 0000000..b2b9ff7 --- /dev/null +++ b/assets/css/components/_btn-group-mobile.scss @@ -0,0 +1,48 @@ +.btn--group__mobile{ + z-index: calc(var(--z-header) - 10); + opacity: 0; + transition: opacity .3s ease-in; + + &.is-visible{ + opacity: 1; + + } + + position: fixed; + bottom: 0; + left: 0; + width: 100vw; + height: calc(var(--h-block) * 3); + padding: calc(var(--spacing) * 0.75) var(--padding-body); + // padding-top: var(--spacing); + background-color: var(--color-bg); + background: linear-gradient(0deg, + var(--color-bg) 0%, + var(--color-bg) 75%, + transparent 100%); + + // background-color: yellow; + + display: flex; + align-items: flex-end; + gap: var(--padding-inner); + button, .dropdown{ + flex-grow: 1; + width: 100%; + } + + + + + +} + + +@media #{$small-up}{ + .btn--group__mobile{ + display: none; + opacity: 0!important; + + } +} + diff --git a/assets/css/components/_buttons.scss b/assets/css/components/_buttons.scss index 9ae38bb..4738fd3 100644 --- a/assets/css/components/_buttons.scss +++ b/assets/css/components/_buttons.scss @@ -3,8 +3,12 @@ button{ font-family: var(--font); font-size: var(--fons-normal); color: var(--color-txt); + svg{ + fill: var(--color-txt); + } &:hover{ color: var(--grey-100); + svg{ fill: var(--grey-100); } } a{ @@ -30,22 +34,88 @@ button:disabled{ } +.btn--small{ + height: calc(var(--h-block)*1); + border: var(--border-light); + border-radius: var(--radius-btn); + font-size: var(--fs-small); + font-weight: 500; + line-height: 1; + overflow: hidden; + white-space: nowrap; + + a{ + display: flex; + align-items: center; + justify-content: center; + gap: 1ch; + width: 100%; + height: 100%; + padding: 0 1ch; + padding-top: 2px; + } + + .icon{ + --size: 10px; + height: var(--size); + width: var(--size); + position: relative; + top: -8px; + svg{ + width: 100%; + fill: var(--color-txt); + } + } + + &.no-link{ + display: flex; + align-items: center; + justify-content: center; + gap: 1ch; + padding: 0 1ch; + padding-top: 2px; + + } + &:hover{ + color: currentColor; + border-color: currentColor; + background-color: var(--grey-950); + + } +} + +.btn--small.is-selected{ + background-color: var(--color-txt); + border-color: var(--color-txt); + color: var(--color-bg); + a{ color: var(--color-bg); } + svg{ fill: var(--color-bg); } +} + .btn--bold, .btn--bold-inline{ display: block; height: calc(var(--h-block)*1); border: var(--border); border-radius: var(--radius-btn); - font-size: var(--fs-small); + font-size: var(--fs-xsmall); + font-weight: 500; text-transform: uppercase; line-height: 1; overflow: hidden; white-space: nowrap; + svg{ + width: 18px; + height: 18px; + position: relative; + top: -1px; + } a{ display: flex; align-items: center; justify-content: center; + gap: 1ch; width: 100%; height: 100%; padding: 0 2ch; @@ -55,10 +125,12 @@ button:disabled{ display: flex; align-items: center; justify-content: center; + gap: 1ch; padding: 0 2ch; padding-top: 4px; } + } @@ -67,112 +139,130 @@ button:disabled{ .btn--bold{ background-color: var(--color-txt); color: var(--color-bg); + svg{ fill: var(--color-bg); } &:hover{ background-color: var(--color-accent); border-color: var(--color-accent); a{ color: var(--color-bg); } + svg{ fill: var(--color-bg); } } } .btn--bold-inline{ background-color: var(--color-bg); + svg{ fill: var(--color-txt); } + &:hover{ + background-color: var(--grey-950); + color: var(--grey-100); + border-color: var(--grey-100); + a{ + background-color: var(--grey-950); + color: var(--grey-100); + } + svg{ fill: var(--grey-100); } + } + } -.btn--bold-inline:hover{ - background-color: var(--grey-800); - color: var(--color-txt); - a{ - background-color: var(--grey-800); - color: var(--color-txt); + + + + +.btn--toc{ + svg{ + width: 15px; + height: 15px; + top: 0px; } } - - - - -// DELETE ? - -.btn__default{ - - - --size: calc(var(--h-block) - 8px); - font-size: var(--fs-normal); - font-weight: var(--fw-normal); - height: var(--size); - padding-right: 1.5ch; - +@mixin btn--go-to(){ position: relative; - - - display: flex; - align-items: center; - gap: 0ch; - // padding-right: 0.5ch; - color: var(--color-accent); - font-weight: var(--fw-medium); - text-decoration: none; - - cursor: pointer; - - - .icon, .txt{ z-index: 10; } - - .icon{ - width: var(--size); - height: var(--size); - display: flex; - align-items: center; - justify-content: center; - color: var(--color-bg); - text-align: center; - + .btn--go-to{ + position: absolute; + right: var(--padding-inner); + bottom: calc(var(--padding-inner) - 3px); svg{ - fill: var(--color-bg); - width: 80%; + width: 15px; + height: 15px; + fill: var(--color-txt); + } + @media #{$small}{ + svg{ + width: 11px; + height: 11px; + } + } + @media #{$x-small}{ + right: calc(var(--padding-inner)*0.5); + bottom: calc(var(--padding-inner)*0.25); + svg{ + width: 11px; + height: 11px; + } } } + &:hover{ + .btn--go-to{ + animation: wiggle-left 0.8s ease-in-out; + } + } + - .txt{ - font-family: var(--font-title); - color: var(--color-accent); - font-size: var(--fs-normal); - font-weight: var(--fw-bold); - padding-left: 1ch; + +} + + + + +@keyframes wiggle-left { + 0% { transform: translateX(0); } + 40% { transform: translateX(-10px); } + 80% { transform: translateX(0); } + 100% { transform: translateX(0); } +} + + + +.btn--back-to-top{ + display: flex; + border-color: var(--color-txt); + width: fit-content; + margin: var(--spacing) auto; + font-size: var(--fs-xsmall); + @media #{$small}{ + margin-bottom: calc(var(--spacing)*2); } - &::after{ - content: ''; - display: block; - background-color: var(--color-accent); - border-radius: calc(var(--size)/2); - width: var(--size); - height: var(--size); - position: absolute; - left: 0; - z-index: 0; - transition: width .2s + .icon{ + width: 12px; + height: 12px; + transform: rotate(-90deg); + transform-origin: center; + position: relative; + top: -1px; + svg{ + width: 12px; + height: 12px; + } } &:hover{ - // - .txt{ - color: var(--color-bg); - display: block; - } - &::after{ - width: 100%; + background-color: var(--grey-950); + color: var(--grey-100); + border-color: var(--grey-100); + a{ + background-color: var(--grey-950); + color: var(--grey-100); } + svg{ fill: var(--grey-100); } } - - - - - - } + + + diff --git a/assets/css/components/_card-article-small.scss b/assets/css/components/_card-article-small.scss index ee37d96..7d64cc9 100644 --- a/assets/css/components/_card-article-small.scss +++ b/assets/css/components/_card-article-small.scss @@ -1,66 +1,100 @@ - - -.card--article-small{ +.card--article-small { @include grid-content(); - margin-bottom: var(--spacing); - + + padding-top: calc(var(--spacing)*0.5); + padding-bottom: calc(var(--spacing)*0.5); + + border-bottom: var(--border-light); + + &:first-of-type { + border-top: var(--border-light); + } + + &.has-link { + @include hover-card-line(); + } @include figure-16-9(); - - .content{ + + figure { + @media #{$x-small} { + margin-left: 0px; + } + } + + + .content { display: flex; flex-direction: column; padding-top: calc(var(--spacing)*0.25); + padding-right: calc(var(--padding-inner)*3); + } + + .title { + font-weight: 500; + font-size: var(--fs-small); + margin-bottom: 0.25em; + text-wrap: balance; + max-width: 42ch; + text-transform: uppercase; + line-height: var(--leading-title); + + a { + text-decoration: none; + } } - .title{ - font-weight: normal; - font-size: var(--fs-medium); - margin-bottom: 0.25em; - a{ text-decoration: none;} - - } - - time{ + time { flex-grow: 1; color: var(--color-txt-light); } - .keywords{ - margin-top: 1.5em; - padding-bottom: calc(var(--spacing)*0.25); - color: var(--color-txt-light); + + .description { + @include clamp(2); + margin-bottom: calc(var(--spacing)*0.75); + margin-top: calc(var(--spacing)*0.25); } - &:hover{ - .title{ text-decoration: underline;} + + + @include btn--go-to(); + @include hover-card-line(); + + .keywords { + display: none; } - @media #{$medium}{ - .title{ + @media #{$medium} { + .title { font-size: var(--fs-normal); margin-bottom: 0; } } - @media #{$x-small}{ - .content{ - padding: 0; + @media #{$x-small} { + + .content { + display: contents; } - .title{ - margin-bottom: 0; - } - time{ + + .title { font-size: var(--fs-small); - margin-top: 0.25em; + padding-top: calc(var(--spacing)*0.25); } - .keywords{ - margin-top: 0.5em; - padding-bottom: 0; + + figure { + grid-row: 1/3; } + + .description { + grid-column: span 2; + } + } - + + } \ No newline at end of file diff --git a/assets/css/components/_card-article.scss b/assets/css/components/_card-article.scss index 9b6bbbf..a9a1ad7 100644 --- a/assets/css/components/_card-article.scss +++ b/assets/css/components/_card-article.scss @@ -4,39 +4,65 @@ display: flex; flex-direction: column; + padding: var(--padding-inner); + + @include figure-16-9(); + @include pin(); + + & > figure{ + width: calc(100% + var(--padding-inner)*2); + position: relative; + left: calc(var(--padding-inner)*-1); + top: calc(var(--padding-inner)*-1); + } .content{ - padding: var(--padding-inner); + // padding: var(--padding-inner); display: flex; flex-direction: column; - flex-grow: 2; + } + .pin{ + position: absolute; + top: var(--padding-inner); + right: var(--padding-inner); + } + + .time-alone{ + display: none; + margin-top: calc(var(--spacing)*0.25); + margin-bottom: calc(var(--spacing)*0.75); } .title{ - margin-top: calc(var(--spacing)*0.5); - font-size: var(--fs-medium); - font-weight: normal; + margin-top: calc(var(--spacing)*0.75); + font-size: var(--fs-normal); + line-height: var(--leading-title); + font-weight: 500; text-transform: uppercase; - flex-grow: 2; + text-wrap: balance; + // flex-grow: 2; a{ text-decoration: none;} } .description{ - margin-top: calc(var(--spacing)*1); - font-size: var(--fs-small); + margin-top: calc(var(--spacing)*0.5); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; + + font-size: var(--fs-small); } .dl{ margin-top: calc(var(--spacing)*0.5); - + // font-size: var(--fs-small); + border-bottom: var(--border-light); + font-size: var(--fs-small); .dl__group{ @include grid-content(); border-top: var(--border-light); @@ -48,24 +74,37 @@ padding-right: 1ch; } - ul:not(.keywords){ + ul{ list-style: none; - li{ - padding-bottom: 0.2em; - } - + li{ padding-bottom: 0.2em; } } - .dl__group__keywords{ - padding-bottom: 0; - } + } + + + .keywords-wrapper{ + z-index: 3000; + margin-top: calc(var(--spacing)*0.5); + // margin-bottom: calc(var(--spacing)*0.5); + } + + .keywords{ + + grid-column: 2; } &:hover{ border-color: var(--color-txt); + background-color: var(--grey-950); } + .link-block{ + z-index: 2000; + } + + + } \ No newline at end of file diff --git a/assets/css/components/_card-impact-small.scss b/assets/css/components/_card-impact-small.scss new file mode 100644 index 0000000..cb84bd6 --- /dev/null +++ b/assets/css/components/_card-impact-small.scss @@ -0,0 +1,96 @@ +.card--impact-small { + + border-bottom: var(--border-light); + &:first-of-type{ + border-top: var(--border-light); + } + + + @include grid-content(); + padding: var(--padding-inner) 0; + + &.has-link { + @include hover-card-line(); + } + + @include btn--go-to(); + + .btn--go-to { + top: calc(var(--padding-inner)*1); + bottom: auto; + } + + .content { + grid-column: 2; + } + + .card--open-graph { + grid-column: 2; + z-index: 10; + } + + .keywords { + grid-column: 2; + z-index: 10; + @media #{$x-small} { display: none; } + + + } + + &:not([data-impact-type="media"]) .content, + .keywords { + @media #{$x-small} { + grid-column: span 2; + } + } + + .tag { + width: auto; + justify-self: start; + position: relative; + top: -5px; + } + + &:not([data-impact-type="media"]) { + .content { + padding-right: calc(var(--padding-inner)*2.5); + @media #{$x-small} { + padding-right: 0; + } + } + + } + + + .open-graph__details { + grid-column: 2; + @include details-summary(); + + summary { + color: var(--color-txt-light); + + .arrow-details svg { + fill: var(--color-txt-light); + } + } + + @media #{$x-small} { + summary { + @include grid-content(); + grid-row-gap: 0; + + .summary-inner { + grid-column: 2; + } + } + + grid-column: 1/3; + } + } + + + + +} + + diff --git a/assets/css/components/_card-impact.scss b/assets/css/components/_card-impact.scss index 90ba5d1..0994562 100644 --- a/assets/css/components/_card-impact.scss +++ b/assets/css/components/_card-impact.scss @@ -1,77 +1,85 @@ -.card--impact{ - @include grid-content(); - padding: calc(var(--spacing)*0.5) 0; - border-bottom: var(--border-light); - &:first-of-type{ - border-top: var(--border-light); +.card--impact { + + container-type: inline-size; + container-name: impact; + border: var(--border-light); + margin-bottom: calc(var(--padding-body)*1); + padding: var(--padding-inner); + + display: flex; + flex-direction: column; + align-items: start; + justify-content: start; + gap: calc(var(--spacing)*0.25); + + + @include hover-card-line(); + @include figure-3-1(); + @include pin(); + + .title{ + font-size: var(--fs-normal); + font-weight: 500; + text-transform: uppercase; + line-height: var(--leading-title); + padding-top: calc(var(--spacing)*0.5); + margin-right: 2ch; } - .content{ - grid-column: 2; + .date{ + font-size: var(--fs-small); + margin-top: calc(var(--spacing)*1.5); } - &:not([data-impact-type="media"]) .content{ - @media #{$x-small}{ grid-column: span 2; } + + .investigations{ + text-decoration: none; + list-style: none; + li{ + font-size: var(--fs-small); + color: var(--color-txt-light); + a{ + text-decoration: none; + } + a::before{ + content: "↪ " + } + } + } + + .card--open-graph{ + width: 100%; + margin: calc(var(--spacing)*0.5) 0; + } + + .pin{ + position: absolute; + top: var(--padding-inner); + right: var(--padding-inner); + } + + & > figure{ + width: calc(100% + var(--padding-inner)*2); + position: relative; + left: calc(var(--padding-inner)*-1); + top: calc(var(--padding-inner)*-1); } .tag{ - - width: auto; - justify-self: start; position: relative; - top: -5px; - + top: 3px; } - .content{ - .see-more{ - color: var(--color-txt-light); - text-decoration: none; - white-space: nowrap; - &::after{ - content: ' +' - } - } - } - - - .open-graph__details{ - grid-column: span 2; - @include details-summary(); - summary{ - color: var(--color-txt-light); - .arrow-details svg{ fill: var(--color-txt-light);} - } - } - - - // @media #{$x-small-up}{ - - .open-graph__details{ - summary{ - @include grid-content(); - .summary-inner{ - grid-column: 2; - - } - } - - } - .open-graph__inner{ - margin-top: calc(var(--spacing)*0.5); - margin-bottom: calc(var(--spacing)*1); - @media #{$small-up}{ - margin-bottom: calc(var(--spacing)*2); - } - .card--open-graph{ - margin-bottom: calc(var(--spacing)*0.5); - } - - } - - - // } - - -} \ No newline at end of file + +} + + +@media #{$small-up}{ +.card--impact, +.grid-sizer{ + width: calc(50% - 13px); +} + +} + diff --git a/assets/css/components/_card-open-graph.scss b/assets/css/components/_card-open-graph.scss index 2b028b3..ff75674 100644 --- a/assets/css/components/_card-open-graph.scss +++ b/assets/css/components/_card-open-graph.scss @@ -1,19 +1,36 @@ .card--open-graph{ - @include grid-content(); - - border: var(--border-light); + container-type: inline-size; + container-name: opengraph; + + .open-graph__inner{ + @include grid-content(); + + } + + + + + + // border: var(--border-light); + border: 1px solid var(--grey-600); border-radius: var(--radius-small); position: relative; figure{ grid-column: 1; grid-row: 1; - + width: 100%; + height: 100%; + img{ + width: 100%; + height: 100%; + object-fit: cover; + } } - @include figure-16-9(); + // @include figure-16-9(); @media #{$x-small}{ @@ -24,20 +41,25 @@ } .content{ - padding: var(--padding-inner); - padding-left: 0; + padding-top: calc(var(--padding-inner)*0.75); + padding-bottom: calc(var(--padding-inner)*0.75); + + padding-right: var(--padding-inner); grid-column: 2; grid-row: 1; } .site-name{ color: var(--color-txt-light); - font-size: var(--fs-small); + font-size: var(--fs-xsmall); + margin-bottom: 3px; } .title{ - font-size: var(--fs-normal); - font-weight: normal; + font-size: var(--fs-small); + font-weight: 500; + line-height: 1.1; + padding-top: 2px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; @@ -49,7 +71,7 @@ } .description{ - font-size: var(--fs-small); + font-size: var(--fs-xsmall); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; @@ -57,7 +79,23 @@ } &:hover{ - .title{ text-decoration: underline;} - + border-color: var(--color-txt); + background-color: var(--grey-950); } +} + + + +@container opengraph (width < 500px) { + // .card--open-graph{ + // background-color: red!important; + + // .content{ + // padding: calc(var(--padding-inner)*0.5); + // } + + // figure{ + // aspect-ratio: inherit; + // } + // } } \ No newline at end of file diff --git a/assets/css/components/_card-package.scss b/assets/css/components/_card-package.scss new file mode 100644 index 0000000..0264d68 --- /dev/null +++ b/assets/css/components/_card-package.scss @@ -0,0 +1,145 @@ +.card--package { + position: relative; + max-width: var(--max-w-cards); + border: var(--border-light); + padding: var(--padding-inner); + @include grid-content(); + @include figure-16-9(); + + container-type: inline-size; + container-name: cardpackage; + + figure { + border-radius: var(--radius-small); + background-color: var(--color-accent); + + img { + opacity: 0.8; + filter: grayscale(1); + } + } + + .content { + display: flex; + flex-direction: column; + font-size: var(--fs-small); + } + + .title { + font-weight: 500; + font-size: var(--fs-normal); + line-height: var(--leading-title); + + margin-bottom: 0.75em; + text-wrap: balance; + max-width: 42ch; + text-transform: uppercase; + padding-top: calc(var(--spacing) * 0.25); + a { + text-decoration: none; + } + + @media #{$small} { + font-size: var(--fs-normal); + } + + .icon { + padding-right: 1ch; + position: relative; + top: 1px; + svg { + height: 15px; + width: 15px; + fill: var(--color-txt); + } + } + } + + .short { + @include clamp(3); + flex-grow: 1; + padding-right: calc(var(--padding-inner) * 2); + } + + ul { + display: flex; + list-style: none; + gap: 1ch; + color: var(--color-txt-light); + padding-top: calc(var(--spacing) * 0.5); + + li + li { + &::before { + content: "|"; + padding-right: 1ch; + } + } + @media #{$small} { + font-size: var(--fs-small); + } + } + + .btn--go-to { + position: absolute; + right: calc(var(--padding-inner) * 1); + bottom: var(--padding-inner); + } + + @include btn--go-to(); + + &:hover { + background-color: var(--grey-950); + border-color: var(--color-txt); + } +} + +[data-template="investigation-summary"] .card--package { + border: none; + padding-left: 0; + padding-right: 0; + border-bottom: var(--border-light); + + &:first-of-type { + border-top: var(--border-light); + } + &.has-link { + @include hover-card-line(); + } + + figure { + @media #{$x-small} { + margin-left: 0px; + } + } + + .title { + font-size: var(--fs-small); + } +} + +@media #{$x-small} { + .card--package { + .content { + display: contents; + } + figure { + grid-row: 1/3; + } + + .title { + font-size: var(--fs-small); + padding-top: calc(var(--spacing) * 0.25); + margin-bottom: 0; + } + + .short { + grid-column: span 2; + } + + ul { + grid-column: 2; + grid-row: 2; + padding-top: 0; + } + } +} diff --git a/assets/css/components/_details-summary.scss b/assets/css/components/_details-summary.scss index f556811..e73d365 100644 --- a/assets/css/components/_details-summary.scss +++ b/assets/css/components/_details-summary.scss @@ -31,6 +31,13 @@ } + summary:hover{ + color: var(--color-txt); + .arrow-details svg{ + fill: var(--color-txt); + } + } + &[open]{ .arrow-details svg{ transform: rotate(90deg); diff --git a/assets/css/components/_dropdown.scss b/assets/css/components/_dropdown.scss new file mode 100644 index 0000000..0c813c8 --- /dev/null +++ b/assets/css/components/_dropdown.scss @@ -0,0 +1,170 @@ +.dropdown { + position: relative; + display: inline-block; + + &__trigger { + cursor: pointer; + } + + &__content { + position: absolute; + top: 100%; + left: 0; + min-width: 180px; + margin-top: var(--padding-inner); + background-color: var(--color-bg); + border: var(--border); + border-radius: var(--radius-btn); + opacity: 0; + visibility: hidden; + transform: translateY(-4px); + transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s; + z-index: 100; + + &::before{ + content: "◀"; + transform: rotate(90deg); + font-size: 14px; + position: absolute; + top: -13px; + left: 16px; + } + ul { + list-style: none; + margin: 0; + padding: var(--padding-inner); + } + + a, button { + display: block; + width: 100%; + padding: 0.75em 1ch; + font-size: var(--fs-small); + text-align: left; + text-decoration: none; + color: var(--color-txt); + background: none; + border: none; + cursor: pointer; + + &:hover { + background-color: var(--grey-800); + } + } + } + + // Positionnement par défaut : sous le bouton, aligné à gauche + // Variante : aligné à droite + &--align-right .dropdown__content { + left: auto; + right: 0; + &::before{ + left: auto; + right: 16px; + + } + } + + &.is-open { + .dropdown__content { + opacity: 1; + visibility: visible; + transform: translateY(0); + } + } + + // PANEL  + &--position-mobile .dropdown__content { + top: auto; + bottom: calc(var(--h-block) + var(--padding-inner)*2); + left: auto; + right: 0; + margin-top: 0; + margin-left: 4px; + + &::before{ + font-family: Arial; + content: "◀"; + transform: rotate(-90deg); + font-size: 14px; + position: absolute; + top: auto; + bottom: -13px; + left: auto; + right: 10%; + } + } + + + + &--position-mobile.is-open { + .dropdown__content { + transform: translateX(0); + } + } + + + + + @media #{$small-up}{ + &--position-panel .dropdown__content { + top: auto; + bottom: 0; + left: calc(100% + var(--padding-inner)); + margin-top: 0; + margin-left: 4px; + + &::before{ + font-family: Arial; + content: "◀"; + transform: rotate(0deg); + font-size: 14px; + position: absolute; + top: auto; + bottom: 4px; + left: -11px; + } + } + + + + &--position-panel.is-open { + .dropdown__content { + transform: translateX(0); + } + } + + } + + + @media #{$x-small}{ + + .dropdown__content{ + width: calc(100vw - var(--padding-body)*2); + .modal--share{ + width: 100%; + } + } + + + } + +} + + + +@media screen and (max-width: 1280px){ + + [data-template="investigations"]{ + .dropdown .dropdown__content { + left: auto; + right: 0; + &::before{ + left: auto; + right: 16px; + + } + } + } + +} diff --git a/assets/css/components/_figures.scss b/assets/css/components/_figures.scss index 8be72a8..aed5e44 100644 --- a/assets/css/components/_figures.scss +++ b/assets/css/components/_figures.scss @@ -1,6 +1,7 @@ @mixin figure-16-9{ - figure{ + & > picture, + & > figure{ aspect-ratio: 16/9; display: flex; overflow: hidden; @@ -12,7 +13,52 @@ } } - &:hover{ - figure img{ transform: scale(1.05); } +} + + + +@mixin figure-3-1(){ + & > picture, + & > figure{ + aspect-ratio: 3/1; + overflow: hidden; + img{ + width: 100%; + height: 100%; + object-fit: cover; + } } -} \ No newline at end of file +} + + + +// @mixin figure-2-1(){ +// & > picture, +// & > figure{ +// aspect-ratio: 2/1; +// overflow: hidden; +// img{ +// width: 100%; +// height: 100%; +// object-fit: cover; +// } +// } +// } + +// @mixin figure-16-9-hover{ +// figure{ +// aspect-ratio: 16/9; +// display: flex; +// overflow: hidden; +// img{ +// width: 100%; +// height: 100%; +// object-fit: cover; +// transition: cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.5s; + +// } +// } +// &:hover{ +// figure img{ transform: scale(1.05); } +// } +// } \ No newline at end of file diff --git a/assets/css/components/_form-newsletter.scss b/assets/css/components/_form-newsletter.scss index bc5d931..a00e9bf 100644 --- a/assets/css/components/_form-newsletter.scss +++ b/assets/css/components/_form-newsletter.scss @@ -18,14 +18,14 @@ padding: 0 2ch; font-family: var(--font); - font-size: var(--fs-normal); + font-size: var(--fs-small); z-index: 40; padding-top: 4px; background: var(--color-bg); color: var(--color-txt); &::placeholder{ font-family: var(--font); - font-size: var(--fs-normal); + font-size: var(--fs-small); color: var(--color-txt); } @@ -49,7 +49,7 @@ --size: calc(var(--h-block)*1.25 - 8px); font-family: var(--font); - font-size: var(--fs-button-bold); + font-size: var(--fs-small); height: var(--size); @@ -83,7 +83,7 @@ .txt{ position: relative; top: 2px; - font-size: var(--fs-normal); + font-size: var(--fs-small); display: none; padding-left: 1ch; } diff --git a/assets/css/components/_keywords.scss b/assets/css/components/_keywords.scss index d7dbe5d..cd1f648 100644 --- a/assets/css/components/_keywords.scss +++ b/assets/css/components/_keywords.scss @@ -1,14 +1,50 @@ .keywords{ list-style: none; - li{ - display: inline-block; - padding-right: 0.75ch; - a{ - text-decoration: none; - &::before{ - content: "#"; - padding-right: 0.25ch; - } + display: flex; + flex-wrap: wrap; + gap: 0.75ch; + + a{ + display: inline-flex; + align-items: center; + justify-content: center; + height: calc(var(--h-block)*0.75); + padding: 0 1ch; + padding-top: 2px; + border: var(--border-light); + border-radius: var(--radius-btn); + font-size: var(--fs-xsmall); + line-height: 1; + white-space: nowrap; + text-decoration: none; + &::before{ + content: "#"; + padding-right: 0.25ch; } + &:hover{ + border-color: currentColor; + background-color: var(--grey-800); + } + + } +} + + +.keywords--small{ + list-style: none; + + li{ + display: inline; + } + + a{ + text-decoration: none; + &::before{ + content: "#"; + padding-right: 0.25ch; + } + &::after{ + content: "\00a0"; + } } } \ No newline at end of file diff --git a/assets/css/components/_list-socials.scss b/assets/css/components/_list-socials.scss index 6716e48..1a6670f 100644 --- a/assets/css/components/_list-socials.scss +++ b/assets/css/components/_list-socials.scss @@ -11,16 +11,16 @@ } .icon{ - width: 24px; - height: 24px; + width: 20px; + height: 20px; position: relative; top: -2px; } svg{ display: flex; align-items: center; - width: 24px; - height: 24px; + width: 20px; + height: 20px; } @@ -36,8 +36,10 @@ display: block; li{ - margin-bottom: calc(var(--spacing)*0.5); + margin-bottom: calc(var(--spacing)*0.25); break-inside: avoid; + font-size: var(--fs-small); + } a{ @@ -48,6 +50,14 @@ content: '↗'; color: var(--grey-300); } + + &:hover{ + color: var(--color-accent); + &::after{ + color: var(--color-accent); + opacity: 0.5; + } + } } .text{ display: block; diff --git a/assets/css/components/_modal-share.scss b/assets/css/components/_modal-share.scss index c6375c5..b28b375 100644 --- a/assets/css/components/_modal-share.scss +++ b/assets/css/components/_modal-share.scss @@ -1,13 +1,21 @@ .modal--share{ - border: var(--border); - border-radius: var(--radius-btn); + width: 240px; + padding-bottom: var(--padding-inner); background-color: var(--color-bg); - padding: var(--padding-inner); - padding-top: calc( var(--padding-inner)*0.5); - padding-bottom: calc( var(--padding-inner)*1.5); - .modal-title{ - display: none; + .title { + font-size: var(--fs-xsmall); + line-height: 1.2; + padding: var(--padding-inner); + padding-bottom: 0px; + @include clamp(2); + font-style: italic; + &::before{ + content: "« "; + } + &::after{ + content: " »"; + } } .socials{ @@ -15,20 +23,22 @@ list-style: none; li{ - font-size: var(--fs-normal); - padding-bottom: 6px; - border-bottom: var(--border-light); - padding-top: 6px; - - a{ + font-size: var(--fs-xsmall); + border-bottom: var(--border-light); + &:first-of-type{ + border-top: var(--border-light); + } + + a{ display: flex; align-items: center; - gap: 1ch; + gap: 2ch; text-decoration: none; + font-size: var(--fs-xsmall); } .icon { - width: 20px; - height: 20px; + width: 16px; + height: 16px; svg{ width: 100%; height: 100%; @@ -38,27 +48,20 @@ position: relative; top: 2px; } - &:hover{ - color: var(--color-accent); - - .icon svg, - .icon svg path, - .icon svg rect{ - fill: var(--color-accent)!important; - } - } } + + - - } .copy-link{ display: flex; gap: 0.5ch; - margin-top: calc(var(--spacing)*0.75); + padding: 0 var(--padding-inner); + height: var(--h-block); + width: 100%; .copy-link__field{ flex-grow: 1; @@ -83,7 +86,7 @@ width: 12px; } input{ - font-size: var(--fs-small); + font-size: var(--fs-xsmall); font-family: var(--font); background: none; border: none; @@ -99,24 +102,63 @@ &:focus{ border-color: var(--color-txt); outline: none; - + } + &.is-copied{ + color: var(--color-accent)!important; } } - button{ + .copy-link__btn{ background-color: var(--color-txt); color: var(--color-bg); border-radius: var(--radius-btn); padding: 0 1ch; - font-size: var(--fs-small); + font-size: var(--fs-xsmall); padding-top: 2px; + white-space: nowrap; + text-align: center; + + width: 8ch!important; + + &::after{ + content: "\00a0"; + } + &:hover{ background-color: var(--color-accent); - } + } + + } } + + +// SHARE ACTIONS -------------------------------------------------- +// ---------------------------------------------------------------- + +#share-banner__content, +#share-banner__aside, +#share-banner__desktop{ + display: none; +} +#share-banner__content ~ .modal--share, +#share-banner__aside ~ .modal--share, +#share-banner__desktop ~ .modal--share{ + opacity: 0; + pointer-events: none; + transition: opacity .2s ease-in; +} + +#share-banner__content:checked ~ .modal--share, +#share-banner__aside:checked ~ .modal--share, +#share-banner__desktop:checked ~ .modal--share{ + opacity: 1; + pointer-events: auto; +} + + diff --git a/assets/css/components/_search-form.scss b/assets/css/components/_search-form.scss new file mode 100644 index 0000000..ac80a15 --- /dev/null +++ b/assets/css/components/_search-form.scss @@ -0,0 +1,40 @@ + .search-form{ + --icon: 40px; + display: grid; + grid-template-columns: var(--icon) 1fr; + input{ + grid-column: 1/3; + grid-row: 1; + height: calc(var(--h-block) * 1.5); + width: 100%; + background: var(--color-bg); + border: 1px solid var(--color-txt); + padding-left: var(--icon); + font-family: var(--font); + font-size: var(--fs-normal); + color: var(--color-txt); + padding-top: 3px; + &::placeholder{ + font-family: var(--font); + font-size: var(--fs-normal); + color: var(--color-txt); + } + &:focus{ + border-color: var(--color-accent); + outline: none; + } + } + + .icon{ + grid-column: 1; + grid-row: 1; + z-index: 10; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + svg{ + width: 20px; + } + } + } \ No newline at end of file diff --git a/assets/css/components/_slider-before-after.scss b/assets/css/components/_slider-before-after.scss new file mode 100644 index 0000000..149bb34 --- /dev/null +++ b/assets/css/components/_slider-before-after.scss @@ -0,0 +1,86 @@ + +.slider-before-after{ + + width: 100%; + + max-width: 700px; + z-index: 300; + display: grid; + place-content: center; + position: relative; + overflow: hidden; + --position: 50%; + + img { + display: block; + max-width: 100%; + } + + + .image-container { + position: relative; + width: 100%; + // max-width: 800px; + // max-height: 90vh; + // aspect-ratio: 1/1; + } + + + + .slider-image { + width: 100%; + height: 100%; + object-fit: cover; + object-position: left; + } + + .image-before { + position: absolute; + inset: 0; + width: var(--position); + } + + .slider { + position: absolute; + inset: 0; + cursor: pointer; + opacity: 0; + /* for Firefox */ + width: 100%; + height: 100%; + } + + .slider:focus-visible ~ .slider-button { + outline: 5px solid black; + outline-offset: 3px; + } + + .slider-line { + position: absolute; + inset: 0; + width: .2rem; + height: 100%; + background-color: #fff; + /* z-index: 10; */ + left: var(--position); + transform: translateX(-50%); + pointer-events: none; + } + + .slider-button { + position: absolute; + background-color: #fff; + color: black; + padding: .5rem; + border-radius: 100vw; + display: grid; + place-items: center; + top: 50%; + left: var(--position); + transform: translate(-50%, -50%); + pointer-events: none; + /* z-index: 100; */ + box-shadow: 1px 1px 1px hsl(0, 50%, 2%, .5); + } + +} \ No newline at end of file diff --git a/assets/css/components/_sort.scss b/assets/css/components/_sort.scss new file mode 100644 index 0000000..941c82b --- /dev/null +++ b/assets/css/components/_sort.scss @@ -0,0 +1,48 @@ +button.sort{ + .arrow{ + line-height: 0; + --size: 12px; + height: var(--size); + width: var(--size); + display: inline-flex; + align-items: center; + justify-content: center; + position: relative; + top: -2px; + transform: rotate(90deg); + transition: transform 0.2s ease-in; + svg{ + width: 100%; + fill: var(--color-txt); + } + + } + + @media #{$x-small}{ + font-size: var(--fs-xsmall); + + } + + + + + &[data-sort-type="up"]{ + .arrow{ + transform: rotate(-90deg); + } + } + +} + + +.page__sort { + margin-bottom: calc(var(--spacing) * 1); + display: flex; + justify-content: right; + grid-gap: var(--padding-inner); + width: 100%; + max-width: var(--max-w-cards); + margin-inline: auto; + // z-index: calc(var(--z-header) - 100); + +} \ No newline at end of file diff --git a/assets/css/components/_summary-hero.scss b/assets/css/components/_summary-hero.scss new file mode 100644 index 0000000..b7a5168 --- /dev/null +++ b/assets/css/components/_summary-hero.scss @@ -0,0 +1,136 @@ + #summary__hero{ + width: 100%; + position: relative; + left: 0; + + + figcaption{ + color: var(--color-txt-light); + font-size: var(--fs-small); + @media #{$x-small}{ font-size: var(--fs-xsmall); } + padding: calc(var(--spacing)*0.5) var(--padding-body); + padding-bottom: 0; + + + } + + figure{ + width: 100%; + position: relative; + img{ + width: 100%; + aspect-ratio: 2/1; + object-fit: cover; + } + } + + .swiper-slide{ + position: relative; + } + + .swiper-button-prev, .swiper-button-next{ + --swiper-navigation-sides-offset: 32px; + --swiper-navigation-size: 32px; + + @media #{$x-small}{ + --swiper-navigation-sides-offset: 15px; + --swiper-navigation-size: 15px; + } + opacity: 0.8; + + // Position buttons relative to the image (16/9 aspect ratio) + // Calculate: 50% of image height = 50% * (9/16) of width + // Image is 100vw wide, so center at: (100vw * 9/16) / 2 + top: calc(100vw * 9 / 2 - var(--swiper-navigation-size)*0.5); + margin-top: 0; + + svg{ color: white; } + } + + .swiper-pagination{ + position: static; + margin-top: calc(var(--spacing)*0.5); + padding: 0 var(--padding-body); + text-align: center; + + .swiper-pagination-bullet{ + width: 15px; + height: 4px; + border-radius: 2px; + background: var(--color-txt-light); + // opacity: 0.4; + } + + .swiper-pagination-bullet-active{ + background: var(--color-txt); + opacity: 0.8; + } + } + + + .player-container{ + width: 100%; + position: relative; + aspect-ratio: 2/1; + + .extract, video{ + width: 100%; + height: 100%; + object-fit: cover; + position: relative; + } + + + + .video-full{ + width: 100%; + height: 100%; + iframe{ + width: 100%; + height: 100%; + } + display: none; + } + + #hero-play-video{ + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + text-transform: uppercase; + display: flex; + align-items: center; + justify-content: center; + + + .btn--bold{ + display: flex; + align-items: center; + justify-content: center; + gap: 1ch; + padding: 0 1ch; + opacity: 0.8; + &:hover{ + opacity: 1; + } + } + + .text{ + color: black; + line-height: 1; + padding-top: 4px; + } + + svg{ + width: 18px; + height: 18px; + fill: black; + opacity: 0.8; + } + } + + } + + + } \ No newline at end of file diff --git a/assets/css/components/_swiper.scss b/assets/css/components/_swiper.scss new file mode 100644 index 0000000..d5e709d --- /dev/null +++ b/assets/css/components/_swiper.scss @@ -0,0 +1,77 @@ +.swiper { + + + --slide-padding: 30px; + + + .swiper-button-prev, + .swiper-button-next { + --swiper-navigation-size: 32px; + color: var(--color-txt); + background-color: var(--color-bg); + height: 100%; + width: var(--slide-padding); + top: 0px!important; + height: calc(100% - var(--spacing)*1); + // background-color: red; + + svg { + width: 14px; + } + + &.swiper-button-disabled { + opacity: 1; + + svg { + opacity: 0.05; + } + } + } + + .swiper-button-prev { + left: 0px!important; + top: 0px; + justify-content: flex-start; + } + + .swiper-button-next { + right: 0px!important; + top: 0px; + justify-content: flex-end; + + } + + .swiper-slide { + padding-left: var(--slide-padding); + padding-right: var(--slide-padding); + } + + .swiper-pagination { + position: relative; + margin-top: 0px!important; + margin-top: calc(var(--spacing)*0.5)!important; + + .swiper-pagination-bullet { + width: 15px; + height: 4px; + border-radius: 2px; + background: var(--color-txt-light); + } + + .swiper-pagination-bullet-active { + background: var(--color-txt); + } + } + + @media #{$x-small} { + + .swiper-button-prev, + .swiper-button-next { + display: none; + } + + .swiper-slide { + padding: 0px; + } + } +} \ No newline at end of file diff --git a/assets/css/components/_tags.scss b/assets/css/components/_tags.scss index 0f12648..9f94af9 100644 --- a/assets/css/components/_tags.scss +++ b/assets/css/components/_tags.scss @@ -1,15 +1,17 @@ .tag{ height: calc(var(--h-block)*0.75); - border-radius: calc(var(--h-block)*0.75/2); + // border-radius: calc(var(--h-block)*0.75/2); + border-radius: var(--radius-small); - display: flex; + display: inline-flex; align-items: center; justify-content: center; - padding: 0 1.5ch; + padding: 0 1ch; padding-top: 3px; - font-size: var(--fs-small); + font-size: var(--fs-xsmall); line-height: 1; + font-weight: 500; background-color: var(--color-txt); color: var(--color-bg); @@ -17,5 +19,7 @@ overflow: hidden; white-space: nowrap; +} + + -} \ No newline at end of file diff --git a/assets/css/components/_text.scss b/assets/css/components/_text.scss index ed64dad..e69de29 100644 --- a/assets/css/components/_text.scss +++ b/assets/css/components/_text.scss @@ -1,4 +0,0 @@ -.title-page{ - font-size: var(--fs-medium); - -} \ No newline at end of file diff --git a/assets/css/partials/_container-cards.scss b/assets/css/partials/_container-cards.scss new file mode 100644 index 0000000..c2f8ab4 --- /dev/null +++ b/assets/css/partials/_container-cards.scss @@ -0,0 +1,23 @@ +.container-cards{ + max-width: var(--max-w-cards); + margin: 0 auto; + display: grid; + grid-gap: calc(var(--spacing)*1); +} + +.container-cards__investigations { + + @media #{$x-small-up} { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); + grid-auto-rows: minmax(100px, auto); + grid-gap: var(--padding-body); + margin-bottom: 10vh; + } + @media #{$x-small} { + margin-bottom: 10vh; + .card--article { + // margin-bottom: calc(var(--spacing) * 1.5); + } + } + } \ No newline at end of file diff --git a/assets/css/partials/_main-layout.scss b/assets/css/partials/_main-layout.scss index a92ae2b..1c7aa30 100644 --- a/assets/css/partials/_main-layout.scss +++ b/assets/css/partials/_main-layout.scss @@ -1,6 +1,8 @@ body{ min-height: 100dvh; min-height: 100vh; + width: 100vw; + overflow-x: hidden; display: flex; flex-direction: column; @@ -9,6 +11,22 @@ body{ main{ flex-grow: 1; padding: 0 var(--padding-body); + padding-top: var(--header-h); + padding-bottom: calc(var(--spacing)*2); + + min-height: 100dvh; + min-height: 100vh; + + .page__header, + .page__content{ + max-width: var(--max-w-cards); + margin-inline: auto; + } } + + + + + } \ No newline at end of file diff --git a/assets/css/partials/_page-header.scss b/assets/css/partials/_page-header.scss new file mode 100644 index 0000000..476aeea --- /dev/null +++ b/assets/css/partials/_page-header.scss @@ -0,0 +1,59 @@ +.page__type{ + + height: calc(var(--h-block)*0.75); + border-radius: var(--radius-small); + border: var(--border-medium); + + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 1.5ch; + padding-top: 3px; + + font-size: var(--fs-xsmall); + + background-color: var(--color-bg); + color: var(--color-txt-light); + font-weight: 500; + + overflow: hidden; + white-space: nowrap; + text-transform: uppercase; + +} + + + +main .page__header { + margin-top: calc(var(--spacing) * 3); + margin-bottom: calc(var(--spacing) * 2); + + @media #{$small} { + margin-top: calc(var(--spacing) * 2); + + } + + .page__title { + max-width: var(--max-w-content); + text-transform: uppercase; + font-weight: normal; + font-size: var(--fs-medium); + font-weight: 500; + line-height: var(--leading-title); + margin-top: calc(var(--spacing) * 1); + margin-bottom: calc(var(--spacing) * 0.5); + text-wrap: balance; + + } + + .description { + // max-width: 58ch; + // line-height: 1.1; + } + + .description-medium{ + // font-size: var(--fs-medium); + max-width: 58ch; + line-height: 1.1; + } +} diff --git a/assets/css/partials/_site-footer.scss b/assets/css/partials/_site-footer.scss index aa51927..63dd38c 100644 --- a/assets/css/partials/_site-footer.scss +++ b/assets/css/partials/_site-footer.scss @@ -2,7 +2,13 @@ background-color: var(--dark); padding: calc(var(--padding-body)*2) var(--padding-body); + z-index: 500; + .site-footer__container{ + max-width: var(--max-w-cards); + margin: 0 auto; + padding: 0 var(--padding-body); + } .logo { margin-top: calc(var(--spacing)*0.25); @@ -14,6 +20,7 @@ p{ margin: calc(var(--spacing)*0.5) 0; + font-size: var(--fs-small); a{ text-decoration: none; &:hover{ @@ -30,15 +37,18 @@ .footer__mentions{ p{ - font-size: var(--fs-small); + font-size: var(--fs-xsmall); color: var(--color-txt) } } + .footer__socials > p{ + font-weight: 500; + + } @media #{$small}{ - margin-top: calc(var(--spacing)*2); .footer__socials{ @@ -46,6 +56,8 @@ padding-top: calc(var(--spacing)*0.25); border-top: var(--border-light); + + .list-socials{ margin-top: calc(var(--spacing)*0.5); margin-bottom: calc(var(--spacing)*0.75); @@ -56,10 +68,14 @@ border-top: var(--border-light); padding-top: calc(var(--spacing)*0.25); p{ - font-size: var(--fs-small); + font-size: 12px; text-align: center; } } + + .p__small{ + font-size: 12px; + } } @@ -67,15 +83,21 @@ .site-footer__container{ display: grid; - grid-template-columns: 1fr 1fr; + grid-template-columns: 3fr 2fr; column-gap: calc(var(--spacing)*2); - - max-width: 1200px; - margin: 0 auto; + } + + .footer__newsletter{ + grid-column: 1; + grid-row: 2; + } + + .footer__socials{ + grid-column: 2; + grid-row: 2; } .footer__socials .socials{ - max-width: 400px; columns: 2; margin-top: calc(var(--spacing)*1); } diff --git a/assets/css/partials/_site-header.scss b/assets/css/partials/_site-header.scss index aa9adae..e9c4c11 100644 --- a/assets/css/partials/_site-header.scss +++ b/assets/css/partials/_site-header.scss @@ -1,25 +1,24 @@ -@keyframes add-border { - from { - border-bottom-color: transparent; - } - to { - border-bottom: var(--grey-800); - } -} - #site-header { - z-index: 900; + z-index: var(--z-header); --gap: 3ch; + position: fixed; + top: 0; + left: 0; + width: 100vw; height: var(--header-h); background-color: var(--color-bg); padding: 0 var(--padding-body); + box-shadow: -1px 4px 10px 0px var(--color-bg); + .site-header__inner{ width: 100%; height: 100%; + // border-bottom: 1px solid var(--color-bg); + // transition: border-color ease-in .5s; display: flex; align-items: center; @@ -38,6 +37,7 @@ #site-title { flex-grow: 2; + // opacity: 0; svg{ width: 100px; @media #{$small}{ @@ -46,7 +46,7 @@ } } - .title-page{ + .header__title-page{ display: none; flex-grow: 2; text-align: left; @@ -65,6 +65,7 @@ align-items: center; gap: var(--gap); text-transform: uppercase; + font-weight: 500; } @@ -85,6 +86,9 @@ #lang-toggle{ display: flex; gap: 1ch; + button{ + font-weight: 500; + } button:disabled{ color: var(--color-txt-light); } } @@ -92,6 +96,7 @@ cursor: pointer; svg{ width: 30px; + fill: var(--color-txt); } .close{ display: none; } @@ -107,37 +112,11 @@ } + #site-header.is-visible{ - position: fixed; - top: 0; - - .site-header__inner{ - border-bottom: var(--border-light); - - #nav-highlight li:not(.soutenir){ - display: none; - } - #site-title { - @media #{$small-up}{ - flex-grow: 0; - width: calc((100vw - var(--max-w-content))/2 - var(--padding-body)*2); - } - @media #{$medium}{ - width: calc(var(--banner-medium) - var(--padding-body)); - } - } - - .title-page{ - @media #{$small-up}{ - display: block; - } - flex-grow: 2; - } - } - - & ~ main{ - margin-top: var(--header-h); - } + // .site-header__inner{ + // border-color: var(--grey-800); + // } } diff --git a/assets/css/partials/_site-menu.scss b/assets/css/partials/_site-menu.scss index 9e62ee8..b1350b7 100644 --- a/assets/css/partials/_site-menu.scss +++ b/assets/css/partials/_site-menu.scss @@ -6,10 +6,13 @@ top: var(--header-h); right: calc(var(--menu-w)*-1); transition: right .3s ease-in; + @media #{$small}{ + transition: right .4s ease-in; + } background-color: var(--color-bg); border-left: var(--border-light); padding: var(--padding-body); - z-index: 1000; + z-index: calc(var(--z-header) - 1); @media #{$x-small}{ width: 100vw; @@ -20,55 +23,17 @@ flex-direction: column; - form{ + .search-form{ margin-top: calc(var(--spacing)*1); margin-bottom: calc(var(--spacing)*1); - --icon: 40px; - display: grid; - grid-template-columns: var(--icon) 1fr; - input{ - grid-column: 1/3; - grid-row: 1; - height: calc(var(--h-block) * 1.5); - width: 100%; - background: var(--color-bg); - border: 1px solid var(--color-txt); - padding-left: var(--icon); - font-family: var(--font); - font-size: var(--fs-normal); - color: var(--color-txt); - padding-top: 3px; - &::placeholder{ - font-family: var(--font); - font-size: var(--fs-normal); - color: var(--color-txt); - } - &:focus{ - border-color: var(--color-accent); - outline: none; - } - } - - .icon{ - grid-column: 1; - grid-row: 1; - z-index: 10; - width: 100%; - display: flex; - align-items: center; - justify-content: center; - svg{ - width: 20px; - } - } } - nav{ flex-grow: 1; ul{ list-style-type: none; text-transform: uppercase; + font-weight: 500; li{ font-size: var(--fs-normal); a{ @@ -86,14 +51,14 @@ } } } + + .socials{ + padding-bottom: calc(var(--spacing)*0.75); + } } body.menu-open{ - overflow: hidden; - main, footer, #nav-highlight{ - transition: opacity .3s ease-in; - opacity: 0.1; - } + #site-menu{ right: 0; diff --git a/assets/css/style.css b/assets/css/style.css index bb8ac65..38b7cb0 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -2,17 +2,19 @@ :root { --font: "Executive", Arial, sans-serif; --title: "System", Arial, sans-serif; - --fs-xsmall: 10px; - --fs-small: 12px; - --fs-normal: 16px; - --fs-medium: 20px; - --fs-big: 30px; - --fs-xbig: 38px; + --fs-xsmall: 12px; + --fs-small: 16px; + --fs-normal: 20px; + --fs-medium: 30px; + --fs-big: 45px; --fs-button-bold: 22px; - --max-w-content: 700px; - --max-w-container: 1280px; + --max-w-content: 640px; + --max-w-cards: 940px; + --z-header: 2000; + --panel-w: 310px; --leading-tight: 1.05; - --leading-normal: 1.2; + --leading-normal: 1.3; + --leading-title: 1.1; --fw-normal: 400; --fw-medium: 500; --fw-bold: 600; @@ -22,6 +24,7 @@ --grey-400: #969696; --grey-600: #6d6d6d; --grey-800: #383838; + --grey-950: #222222; --color-bg: #161616; --color-txt: #ffffff; --color-txt-light: var(--grey-400); @@ -32,7 +35,7 @@ --color-form: white; --border: 1px solid var(--color-txt); --border-medium: 1px solid var(--grey-600); - --border-light: 1px solid var(--grey-800); + --border-light: 1px solid #414141; --header-h: 80px; --header-h-shrinked: 50px; --menu-w: 420px; @@ -54,24 +57,25 @@ @media screen and (max-width: 768px) { :root { - --fs-xsmall: 10px; - --fs-small: 12px; - --fs-normal: 14px; - --fs-medium: 18px; - --fs-big: 28px; - --fs-xbig: 32px; + --fs-xsmall: 13px; + --fs-small: 16px; + --fs-normal: 20px; + --fs-medium: 24px; + --fs-big: 34px; --header-h: 60px; + --padding-body: 16px; } } :root[data-theme=light] { - --grey-100: #1f1f1f; + --grey-100: #2f2f2f; --grey-200: #2f2f2f; --grey-300: #4a4a4a; --grey-400: #6a6a6a; --grey-600: #9a9a9a; --grey-800: #cfcfcf; + --grey-950: #eaeaea; --color-bg: #efefef; - --color-txt: #000000; + --color-txt: #161616; --color-txt-light: var(--grey-400); --color-accent: #ff00ff; --color-accent-50: #ffe9ff; @@ -111,8 +115,6 @@ body { font-size: var(--fs-normal); color: var(--color-txt); background-color: var(--color-bg); - width: 100vw; - overflow-x: hidden; } img { @@ -129,25 +131,35 @@ img { cursor: pointer; } +.swiper-button-next, .swiper-button-prev, body, #site-header, #site-footer { transition: background-color 0.3s ease, color 0.3s ease; } -@keyframes add-border { - from { - border-bottom-color: transparent; - } - to { - border-bottom: var(--grey-800); - } +body.menu-open, +body.is-hidden { + overflow-y: hidden; } +body.menu-open main, body.menu-open footer, body.menu-open #nav-highlight, body.menu-open .btn--back-to-top, +body.is-hidden main, +body.is-hidden footer, +body.is-hidden #nav-highlight, +body.is-hidden .btn--back-to-top { + transition: opacity 0.3s ease-in; + opacity: 0.1; +} + #site-header { - z-index: 900; + z-index: var(--z-header); --gap: 3ch; + position: fixed; + top: 0; + left: 0; width: 100vw; height: var(--header-h); background-color: var(--color-bg); padding: 0 var(--padding-body); + box-shadow: -1px 4px 10px 0px var(--color-bg); } #site-header .site-header__inner { width: 100%; @@ -174,7 +186,7 @@ body, #site-header, #site-footer { width: 80px; } } -#site-header .title-page { +#site-header .header__title-page { display: none; flex-grow: 2; text-align: left; @@ -191,6 +203,7 @@ body, #site-header, #site-footer { align-items: center; gap: var(--gap); text-transform: uppercase; + font-weight: 500; } #site-header #theme-toggle { width: var(--h-block); @@ -208,6 +221,9 @@ body, #site-header, #site-footer { display: flex; gap: 1ch; } +#site-header #lang-toggle button { + font-weight: 500; +} #site-header #lang-toggle button:disabled { color: var(--color-txt-light); } @@ -216,6 +232,7 @@ body, #site-header, #site-footer { } #site-header #menu-toggle svg { width: 30px; + fill: var(--color-txt); } #site-header #menu-toggle .close { display: none; @@ -224,39 +241,6 @@ body, #site-header, #site-footer { fill: var(--grey-200) !important; } -#site-header.is-visible { - position: fixed; - top: 0; -} -#site-header.is-visible .site-header__inner { - border-bottom: var(--border-light); -} -#site-header.is-visible .site-header__inner #nav-highlight li:not(.soutenir) { - display: none; -} -@media screen and (min-width: 768px) { - #site-header.is-visible .site-header__inner #site-title { - flex-grow: 0; - width: calc((100vw - var(--max-w-content)) / 2 - var(--padding-body) * 2); - } -} -@media screen and (max-width: 1080px) { - #site-header.is-visible .site-header__inner #site-title { - width: calc(var(--banner-medium) - var(--padding-body)); - } -} -#site-header.is-visible .site-header__inner .title-page { - flex-grow: 2; -} -@media screen and (min-width: 768px) { - #site-header.is-visible .site-header__inner .title-page { - display: block; - } -} -#site-header.is-visible ~ main { - margin-top: var(--header-h); -} - @media screen and (max-width: 1080px) { #nav-highlight { display: none; @@ -268,9 +252,15 @@ button { font-size: var(--fons-normal); color: var(--color-txt); } +button svg { + fill: var(--color-txt); +} button:hover { color: var(--grey-100); } +button:hover svg { + fill: var(--grey-100); +} button a { text-decoration: none; width: 100%; @@ -289,23 +279,89 @@ button:disabled { text-decoration: underline !important; } +.btn--small { + height: calc(var(--h-block) * 1); + border: var(--border-light); + border-radius: var(--radius-btn); + font-size: var(--fs-small); + font-weight: 500; + line-height: 1; + overflow: hidden; + white-space: nowrap; +} +.btn--small a { + display: flex; + align-items: center; + justify-content: center; + gap: 1ch; + width: 100%; + height: 100%; + padding: 0 1ch; + padding-top: 2px; +} +.btn--small .icon { + --size: 10px; + height: var(--size); + width: var(--size); + position: relative; + top: -8px; +} +.btn--small .icon svg { + width: 100%; + fill: var(--color-txt); +} +.btn--small.no-link { + display: flex; + align-items: center; + justify-content: center; + gap: 1ch; + padding: 0 1ch; + padding-top: 2px; +} +.btn--small:hover { + color: currentColor; + border-color: currentColor; + background-color: var(--grey-950); +} + +.btn--small.is-selected { + background-color: var(--color-txt); + border-color: var(--color-txt); + color: var(--color-bg); +} +.btn--small.is-selected a { + color: var(--color-bg); +} +.btn--small.is-selected svg { + fill: var(--color-bg); +} + .btn--bold, .btn--bold-inline { display: block; height: calc(var(--h-block) * 1); border: var(--border); border-radius: var(--radius-btn); - font-size: var(--fs-small); + font-size: var(--fs-xsmall); + font-weight: 500; text-transform: uppercase; line-height: 1; overflow: hidden; white-space: nowrap; } +.btn--bold svg, +.btn--bold-inline svg { + width: 18px; + height: 18px; + position: relative; + top: -1px; +} .btn--bold a, .btn--bold-inline a { display: flex; align-items: center; justify-content: center; + gap: 1ch; width: 100%; height: 100%; padding: 0 2ch; @@ -316,6 +372,7 @@ button:disabled { display: flex; align-items: center; justify-content: center; + gap: 1ch; padding: 0 2ch; padding-top: 4px; } @@ -324,6 +381,9 @@ button:disabled { background-color: var(--color-txt); color: var(--color-bg); } +.btn--bold svg { + fill: var(--color-bg); +} .btn--bold:hover { background-color: var(--color-accent); border-color: var(--color-accent); @@ -331,88 +391,98 @@ button:disabled { .btn--bold:hover a { color: var(--color-bg); } +.btn--bold:hover svg { + fill: var(--color-bg); +} .btn--bold-inline { background-color: var(--color-bg); } - +.btn--bold-inline svg { + fill: var(--color-txt); +} .btn--bold-inline:hover { - background-color: var(--grey-800); - color: var(--color-txt); + background-color: var(--grey-950); + color: var(--grey-100); + border-color: var(--grey-100); } .btn--bold-inline:hover a { - background-color: var(--grey-800); - color: var(--color-txt); + background-color: var(--grey-950); + color: var(--grey-100); +} +.btn--bold-inline:hover svg { + fill: var(--grey-100); } -.btn__default { - --size: calc(var(--h-block) - 8px); - font-size: var(--fs-normal); - font-weight: var(--fw-normal); - height: var(--size); - padding-right: 1.5ch; +.btn--toc svg { + width: 15px; + height: 15px; + top: 0px; +} + +@keyframes wiggle-left { + 0% { + transform: translateX(0); + } + 40% { + transform: translateX(-10px); + } + 80% { + transform: translateX(0); + } + 100% { + transform: translateX(0); + } +} +.btn--back-to-top { + display: flex; + border-color: var(--color-txt); + width: -moz-fit-content; + width: fit-content; + margin: var(--spacing) auto; + font-size: var(--fs-xsmall); +} +@media screen and (max-width: 768px) { + .btn--back-to-top { + margin-bottom: calc(var(--spacing) * 2); + } +} +.btn--back-to-top .icon { + width: 12px; + height: 12px; + transform: rotate(-90deg); + transform-origin: center; position: relative; - display: flex; - align-items: center; - gap: 0ch; - color: var(--color-accent); - font-weight: var(--fw-medium); - text-decoration: none; - cursor: pointer; + top: -1px; } -.btn__default .icon, .btn__default .txt { - z-index: 10; +.btn--back-to-top .icon svg { + width: 12px; + height: 12px; } -.btn__default .icon { - width: var(--size); - height: var(--size); - display: flex; - align-items: center; - justify-content: center; - color: var(--color-bg); - text-align: center; +.btn--back-to-top:hover { + background-color: var(--grey-950); + color: var(--grey-100); + border-color: var(--grey-100); } -.btn__default .icon svg { - fill: var(--color-bg); - width: 80%; +.btn--back-to-top:hover a { + background-color: var(--grey-950); + color: var(--grey-100); } -.btn__default .txt { - font-family: var(--font-title); - color: var(--color-accent); - font-size: var(--fs-normal); - font-weight: var(--fw-bold); - padding-left: 1ch; -} -.btn__default::after { - content: ""; - display: block; - background-color: var(--color-accent); - border-radius: calc(var(--size) / 2); - width: var(--size); - height: var(--size); - position: absolute; - left: 0; - z-index: 0; - transition: width 0.2s; -} -.btn__default:hover .txt { - color: var(--color-bg); - display: block; -} -.btn__default:hover::after { - width: 100%; +.btn--back-to-top:hover svg { + fill: var(--grey-100); } .tag { height: calc(var(--h-block) * 0.75); - border-radius: calc(var(--h-block) * 0.75 / 2); - display: flex; + border-radius: var(--radius-small); + display: inline-flex; align-items: center; justify-content: center; - padding: 0 1.5ch; + padding: 0 1ch; padding-top: 3px; - font-size: var(--fs-small); + font-size: var(--fs-xsmall); line-height: 1; + font-weight: 500; background-color: var(--color-txt); color: var(--color-bg); overflow: hidden; @@ -421,18 +491,231 @@ button:disabled { .keywords { list-style: none; + display: flex; + flex-wrap: wrap; + gap: 0.75ch; } -.keywords li { - display: inline-block; - padding-right: 0.75ch; -} -.keywords li a { +.keywords a { + display: inline-flex; + align-items: center; + justify-content: center; + height: calc(var(--h-block) * 0.75); + padding: 0 1ch; + padding-top: 2px; + border: var(--border-light); + border-radius: var(--radius-btn); + font-size: var(--fs-xsmall); + line-height: 1; + white-space: nowrap; text-decoration: none; } -.keywords li a::before { +.keywords a::before { content: "#"; padding-right: 0.25ch; } +.keywords a:hover { + border-color: currentColor; + background-color: var(--grey-800); +} + +.keywords--small { + list-style: none; +} +.keywords--small li { + display: inline; +} +.keywords--small a { + text-decoration: none; +} +.keywords--small a::before { + content: "#"; + padding-right: 0.25ch; +} +.keywords--small a::after { + content: " "; +} + +button.sort .arrow { + line-height: 0; + --size: 12px; + height: var(--size); + width: var(--size); + display: inline-flex; + align-items: center; + justify-content: center; + position: relative; + top: -2px; + transform: rotate(90deg); + transition: transform 0.2s ease-in; +} +button.sort .arrow svg { + width: 100%; + fill: var(--color-txt); +} +@media screen and (max-width: 560px) { + button.sort { + font-size: var(--fs-xsmall); + } +} +button.sort[data-sort-type=up] .arrow { + transform: rotate(-90deg); +} + +.page__sort { + margin-bottom: calc(var(--spacing) * 1); + display: flex; + justify-content: right; + grid-gap: var(--padding-inner); + width: 100%; + max-width: var(--max-w-cards); + margin-inline: auto; +} + +.btn--group__mobile { + z-index: calc(var(--z-header) - 10); + opacity: 0; + transition: opacity 0.3s ease-in; + position: fixed; + bottom: 0; + left: 0; + width: 100vw; + height: calc(var(--h-block) * 3); + padding: calc(var(--spacing) * 0.75) var(--padding-body); + background-color: var(--color-bg); + background: linear-gradient(0deg, var(--color-bg) 0%, var(--color-bg) 75%, transparent 100%); + display: flex; + align-items: flex-end; + gap: var(--padding-inner); +} +.btn--group__mobile.is-visible { + opacity: 1; +} +.btn--group__mobile button, .btn--group__mobile .dropdown { + flex-grow: 1; + width: 100%; +} + +@media screen and (min-width: 768px) { + .btn--group__mobile { + display: none; + opacity: 0 !important; + } +} +#summary__hero { + width: 100%; + position: relative; + left: 0; +} +#summary__hero figcaption { + color: var(--color-txt-light); + font-size: var(--fs-small); + padding: calc(var(--spacing) * 0.5) var(--padding-body); + padding-bottom: 0; +} +@media screen and (max-width: 560px) { + #summary__hero figcaption { + font-size: var(--fs-xsmall); + } +} +#summary__hero figure { + width: 100%; + position: relative; +} +#summary__hero figure img { + width: 100%; + aspect-ratio: 2/1; + -o-object-fit: cover; + object-fit: cover; +} +#summary__hero .swiper-slide { + position: relative; +} +#summary__hero .swiper-button-prev, #summary__hero .swiper-button-next { + --swiper-navigation-sides-offset: 32px; + --swiper-navigation-size: 32px; + opacity: 0.8; + top: calc(450vw - var(--swiper-navigation-size) * 0.5); + margin-top: 0; +} +@media screen and (max-width: 560px) { + #summary__hero .swiper-button-prev, #summary__hero .swiper-button-next { + --swiper-navigation-sides-offset: 15px; + --swiper-navigation-size: 15px; + } +} +#summary__hero .swiper-button-prev svg, #summary__hero .swiper-button-next svg { + color: white; +} +#summary__hero .swiper-pagination { + position: static; + margin-top: calc(var(--spacing) * 0.5); + padding: 0 var(--padding-body); + text-align: center; +} +#summary__hero .swiper-pagination .swiper-pagination-bullet { + width: 15px; + height: 4px; + border-radius: 2px; + background: var(--color-txt-light); +} +#summary__hero .swiper-pagination .swiper-pagination-bullet-active { + background: var(--color-txt); + opacity: 0.8; +} +#summary__hero .player-container { + width: 100%; + position: relative; + aspect-ratio: 2/1; +} +#summary__hero .player-container .extract, #summary__hero .player-container video { + width: 100%; + height: 100%; + -o-object-fit: cover; + object-fit: cover; + position: relative; +} +#summary__hero .player-container .video-full { + width: 100%; + height: 100%; + display: none; +} +#summary__hero .player-container .video-full iframe { + width: 100%; + height: 100%; +} +#summary__hero .player-container #hero-play-video { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + text-transform: uppercase; + display: flex; + align-items: center; + justify-content: center; +} +#summary__hero .player-container #hero-play-video .btn--bold { + display: flex; + align-items: center; + justify-content: center; + gap: 1ch; + padding: 0 1ch; + opacity: 0.8; +} +#summary__hero .player-container #hero-play-video .btn--bold:hover { + opacity: 1; +} +#summary__hero .player-container #hero-play-video .text { + color: black; + line-height: 1; + padding-top: 4px; +} +#summary__hero .player-container #hero-play-video svg { + width: 18px; + height: 18px; + fill: black; + opacity: 0.8; +} .form__newsletter { --size: 24px; @@ -449,7 +732,7 @@ button:disabled { border: 1px solid var(--color-txt); padding: 0 2ch; font-family: var(--font); - font-size: var(--fs-normal); + font-size: var(--fs-small); z-index: 40; padding-top: 4px; background: var(--color-bg); @@ -457,12 +740,12 @@ button:disabled { } .form__newsletter input[type=email]::-moz-placeholder { font-family: var(--font); - font-size: var(--fs-normal); + font-size: var(--fs-small); color: var(--color-txt); } .form__newsletter input[type=email]::placeholder { font-family: var(--font); - font-size: var(--fs-normal); + font-size: var(--fs-small); color: var(--color-txt); } .form__newsletter input[type=email]:focus { @@ -476,7 +759,7 @@ button:disabled { .form__newsletter button[type=submit].btn--newletter { --size: calc(var(--h-block)*1.25 - 8px); font-family: var(--font); - font-size: var(--fs-button-bold); + font-size: var(--fs-small); height: var(--size); display: flex; align-items: center; @@ -505,7 +788,7 @@ button:disabled { .form__newsletter button[type=submit].btn--newletter .txt { position: relative; top: 2px; - font-size: var(--fs-normal); + font-size: var(--fs-small); display: none; padding-left: 1ch; } @@ -529,6 +812,51 @@ button:disabled { width: 100%; } +.search-form { + --icon: 40px; + display: grid; + grid-template-columns: var(--icon) 1fr; +} +.search-form input { + grid-column: 1/3; + grid-row: 1; + height: calc(var(--h-block) * 1.5); + width: 100%; + background: var(--color-bg); + border: 1px solid var(--color-txt); + padding-left: var(--icon); + font-family: var(--font); + font-size: var(--fs-normal); + color: var(--color-txt); + padding-top: 3px; +} +.search-form input::-moz-placeholder { + font-family: var(--font); + font-size: var(--fs-normal); + color: var(--color-txt); +} +.search-form input::placeholder { + font-family: var(--font); + font-size: var(--fs-normal); + color: var(--color-txt); +} +.search-form input:focus { + border-color: var(--color-accent); + outline: none; +} +.search-form .icon { + grid-column: 1; + grid-row: 1; + z-index: 10; + width: 100%; + display: flex; + align-items: center; + justify-content: center; +} +.search-form .icon svg { + width: 20px; +} + .list-socials { list-style: none; display: flex; @@ -542,16 +870,16 @@ button:disabled { text-decoration: none; } .list-socials .icon { - width: 24px; - height: 24px; + width: 20px; + height: 20px; position: relative; top: -2px; } .list-socials svg { display: flex; align-items: center; - width: 24px; - height: 24px; + width: 20px; + height: 20px; } .list-socials .text { display: none; @@ -562,9 +890,10 @@ button:disabled { display: block; } .footer__socials .list-socials li { - margin-bottom: calc(var(--spacing) * 0.5); + margin-bottom: calc(var(--spacing) * 0.25); -moz-column-break-inside: avoid; break-inside: avoid; + font-size: var(--fs-small); } .footer__socials .list-socials a { gap: 1ch; @@ -574,41 +903,61 @@ button:disabled { content: "↗"; color: var(--grey-300); } + .footer__socials .list-socials a:hover { + color: var(--color-accent); + } + .footer__socials .list-socials a:hover::after { + color: var(--color-accent); + opacity: 0.5; + } .footer__socials .list-socials .text { display: block; line-height: 1; } } .modal--share { - border: var(--border); - border-radius: var(--radius-btn); + width: 240px; + padding-bottom: var(--padding-inner); background-color: var(--color-bg); - padding: var(--padding-inner); - padding-top: calc(var(--padding-inner) * 0.5); - padding-bottom: calc(var(--padding-inner) * 1.5); } -.modal--share .modal-title { - display: none; +.modal--share .title { + font-size: var(--fs-xsmall); + line-height: 1.2; + padding: var(--padding-inner); + padding-bottom: 0px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + font-style: italic; +} +.modal--share .title::before { + content: "« "; +} +.modal--share .title::after { + content: " »"; } .modal--share .socials { display: block; list-style: none; } .modal--share .socials li { - font-size: var(--fs-normal); - padding-bottom: 6px; + font-size: var(--fs-xsmall); border-bottom: var(--border-light); - padding-top: 6px; +} +.modal--share .socials li:first-of-type { + border-top: var(--border-light); } .modal--share .socials li a { display: flex; align-items: center; - gap: 1ch; + gap: 2ch; text-decoration: none; + font-size: var(--fs-xsmall); } .modal--share .socials li .icon { - width: 20px; - height: 20px; + width: 16px; + height: 16px; } .modal--share .socials li .icon svg { width: 100%; @@ -618,18 +967,12 @@ button:disabled { position: relative; top: 2px; } -.modal--share .socials li:hover { - color: var(--color-accent); -} -.modal--share .socials li:hover .icon svg, -.modal--share .socials li:hover .icon svg path, -.modal--share .socials li:hover .icon svg rect { - fill: var(--color-accent) !important; -} .modal--share .copy-link { display: flex; gap: 0.5ch; - margin-top: calc(var(--spacing) * 0.75); + padding: 0 var(--padding-inner); + height: var(--h-block); + width: 100%; } .modal--share .copy-link .copy-link__field { flex-grow: 1; @@ -650,7 +993,7 @@ button:disabled { width: 12px; } .modal--share .copy-link input { - font-size: var(--fs-small); + font-size: var(--fs-xsmall); font-family: var(--font); background: none; border: none; @@ -668,69 +1011,248 @@ button:disabled { border-color: var(--color-txt); outline: none; } -.modal--share .copy-link button { +.modal--share .copy-link input.is-copied { + color: var(--color-accent) !important; +} +.modal--share .copy-link .copy-link__btn { background-color: var(--color-txt); color: var(--color-bg); border-radius: var(--radius-btn); padding: 0 1ch; - font-size: var(--fs-small); + font-size: var(--fs-xsmall); padding-top: 2px; + white-space: nowrap; + text-align: center; + width: 8ch !important; } -.modal--share .copy-link button:hover { +.modal--share .copy-link .copy-link__btn::after { + content: " "; +} +.modal--share .copy-link .copy-link__btn:hover { background-color: var(--color-accent); } -.title-page { - font-size: var(--fs-medium); +#share-banner__content, +#share-banner__aside, +#share-banner__desktop { + display: none; } +#share-banner__content ~ .modal--share, +#share-banner__aside ~ .modal--share, +#share-banner__desktop ~ .modal--share { + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease-in; +} + +#share-banner__content:checked ~ .modal--share, +#share-banner__aside:checked ~ .modal--share, +#share-banner__desktop:checked ~ .modal--share { + opacity: 1; + pointer-events: auto; +} + +.dropdown { + position: relative; + display: inline-block; +} +.dropdown__trigger { + cursor: pointer; +} +.dropdown__content { + position: absolute; + top: 100%; + left: 0; + min-width: 180px; + margin-top: var(--padding-inner); + background-color: var(--color-bg); + border: var(--border); + border-radius: var(--radius-btn); + opacity: 0; + visibility: hidden; + transform: translateY(-4px); + transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s; + z-index: 100; +} +.dropdown__content::before { + content: "◀"; + transform: rotate(90deg); + font-size: 14px; + position: absolute; + top: -13px; + left: 16px; +} +.dropdown__content ul { + list-style: none; + margin: 0; + padding: var(--padding-inner); +} +.dropdown__content a, .dropdown__content button { + display: block; + width: 100%; + padding: 0.75em 1ch; + font-size: var(--fs-small); + text-align: left; + text-decoration: none; + color: var(--color-txt); + background: none; + border: none; + cursor: pointer; +} +.dropdown__content a:hover, .dropdown__content button:hover { + background-color: var(--grey-800); +} +.dropdown--align-right .dropdown__content { + left: auto; + right: 0; +} +.dropdown--align-right .dropdown__content::before { + left: auto; + right: 16px; +} +.dropdown.is-open .dropdown__content { + opacity: 1; + visibility: visible; + transform: translateY(0); +} +.dropdown--position-mobile .dropdown__content { + top: auto; + bottom: calc(var(--h-block) + var(--padding-inner) * 2); + left: auto; + right: 0; + margin-top: 0; + margin-left: 4px; +} +.dropdown--position-mobile .dropdown__content::before { + font-family: Arial; + content: "◀"; + transform: rotate(-90deg); + font-size: 14px; + position: absolute; + top: auto; + bottom: -13px; + left: auto; + right: 10%; +} +.dropdown--position-mobile.is-open .dropdown__content { + transform: translateX(0); +} +@media screen and (min-width: 768px) { + .dropdown--position-panel .dropdown__content { + top: auto; + bottom: 0; + left: calc(100% + var(--padding-inner)); + margin-top: 0; + margin-left: 4px; + } + .dropdown--position-panel .dropdown__content::before { + font-family: Arial; + content: "◀"; + transform: rotate(0deg); + font-size: 14px; + position: absolute; + top: auto; + bottom: 4px; + left: -11px; + } + .dropdown--position-panel.is-open .dropdown__content { + transform: translateX(0); + } +} +@media screen and (max-width: 560px) { + .dropdown .dropdown__content { + width: calc(100vw - var(--padding-body) * 2); + } + .dropdown .dropdown__content .modal--share { + width: 100%; + } +} + +@media screen and (max-width: 1280px) { + [data-template=investigations] .dropdown .dropdown__content { + left: auto; + right: 0; + } + [data-template=investigations] .dropdown .dropdown__content::before { + left: auto; + right: 16px; + } +} .card--article { border: var(--border-light); position: relative; display: flex; flex-direction: column; + padding: var(--padding-inner); } -.card--article figure { +.card--article > picture, .card--article > figure { aspect-ratio: 16/9; display: flex; overflow: hidden; } -.card--article figure img { +.card--article > picture img, .card--article > figure img { width: 100%; height: 100%; -o-object-fit: cover; object-fit: cover; transition: cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.5s; } -.card--article:hover figure img { - transform: scale(1.05); +.card--article .pin { + z-index: 90; + width: 18px; + height: 18px; + transform: rotate(45deg); + transform-origin: center; +} +.card--article .pin svg { + width: 100%; + height: 100%; + fill: var(--color-txt); +} +.card--article > figure { + width: calc(100% + var(--padding-inner) * 2); + position: relative; + left: calc(var(--padding-inner) * -1); + top: calc(var(--padding-inner) * -1); } .card--article .content { - padding: var(--padding-inner); display: flex; flex-direction: column; - flex-grow: 2; +} +.card--article .pin { + position: absolute; + top: var(--padding-inner); + right: var(--padding-inner); +} +.card--article .time-alone { + display: none; + margin-top: calc(var(--spacing) * 0.25); + margin-bottom: calc(var(--spacing) * 0.75); } .card--article .title { - margin-top: calc(var(--spacing) * 0.5); - font-size: var(--fs-medium); - font-weight: normal; + margin-top: calc(var(--spacing) * 0.75); + font-size: var(--fs-normal); + line-height: var(--leading-title); + font-weight: 500; text-transform: uppercase; - flex-grow: 2; + text-wrap: balance; } .card--article .title a { text-decoration: none; } .card--article .description { - margin-top: calc(var(--spacing) * 1); - font-size: var(--fs-small); + margin-top: calc(var(--spacing) * 0.5); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; + font-size: var(--fs-small); } .card--article .dl { margin-top: calc(var(--spacing) * 0.5); + border-bottom: var(--border-light); + font-size: var(--fs-small); } .card--article .dl .dl__group { display: grid; @@ -744,17 +1266,25 @@ button:disabled { color: var(--color-txt-light); padding-right: 1ch; } -.card--article .dl ul:not(.keywords) { +.card--article .dl ul { list-style: none; } -.card--article .dl ul:not(.keywords) li { +.card--article .dl ul li { padding-bottom: 0.2em; } -.card--article .dl .dl__group__keywords { - padding-bottom: 0; +.card--article .keywords-wrapper { + z-index: 3000; + margin-top: calc(var(--spacing) * 0.5); +} +.card--article .keywords { + grid-column: 2; } .card--article:hover { border-color: var(--color-txt); + background-color: var(--grey-950); +} +.card--article .link-block { + z-index: 2000; } .card--article-small { @@ -762,32 +1292,69 @@ button:disabled { grid-gap: var(--padding-inner); grid-template-columns: 2fr 3fr; position: relative; - margin-bottom: var(--spacing); + padding-top: calc(var(--spacing) * 0.5); + padding-bottom: calc(var(--spacing) * 0.5); + border-bottom: var(--border-light); + position: relative; + border-bottom: var(--border-light); + position: relative; } -.card--article-small figure { +.card--article-small:first-of-type { + border-top: var(--border-light); +} +.card--article-small.has-link { + border-bottom: var(--border-light); + position: relative; +} +.card--article-small.has-link:first-of-type { + border-top: var(--border-light); +} +.card--article-small.has-link::before { + content: ""; + width: 100%; + border-top: 1px solid transparent; + position: absolute; + top: -1px; + left: 0; +} +.card--article-small.has-link:hover { + background-color: var(--grey-950); + border-color: var(--color-txt); +} +.card--article-small.has-link:hover::before { + border-color: var(--color-txt); +} +.card--article-small > picture, .card--article-small > figure { aspect-ratio: 16/9; display: flex; overflow: hidden; } -.card--article-small figure img { +.card--article-small > picture img, .card--article-small > figure img { width: 100%; height: 100%; -o-object-fit: cover; object-fit: cover; transition: cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.5s; } -.card--article-small:hover figure img { - transform: scale(1.05); +@media screen and (max-width: 560px) { + .card--article-small figure { + margin-left: 0px; + } } .card--article-small .content { display: flex; flex-direction: column; padding-top: calc(var(--spacing) * 0.25); + padding-right: calc(var(--padding-inner) * 3); } .card--article-small .title { - font-weight: normal; - font-size: var(--fs-medium); + font-weight: 500; + font-size: var(--fs-small); margin-bottom: 0.25em; + text-wrap: balance; + max-width: 42ch; + text-transform: uppercase; + line-height: var(--leading-title); } .card--article-small .title a { text-decoration: none; @@ -796,13 +1363,63 @@ button:disabled { flex-grow: 1; color: var(--color-txt-light); } -.card--article-small .keywords { - margin-top: 1.5em; - padding-bottom: calc(var(--spacing) * 0.25); - color: var(--color-txt-light); +.card--article-small .description { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + margin-bottom: calc(var(--spacing) * 0.75); + margin-top: calc(var(--spacing) * 0.25); } -.card--article-small:hover .title { - text-decoration: underline; +.card--article-small .btn--go-to { + position: absolute; + right: var(--padding-inner); + bottom: calc(var(--padding-inner) - 3px); +} +.card--article-small .btn--go-to svg { + width: 15px; + height: 15px; + fill: var(--color-txt); +} +@media screen and (max-width: 768px) { + .card--article-small .btn--go-to svg { + width: 11px; + height: 11px; + } +} +@media screen and (max-width: 560px) { + .card--article-small .btn--go-to { + right: calc(var(--padding-inner) * 0.5); + bottom: calc(var(--padding-inner) * 0.25); + } + .card--article-small .btn--go-to svg { + width: 11px; + height: 11px; + } +} +.card--article-small:hover .btn--go-to { + animation: wiggle-left 0.8s ease-in-out; +} +.card--article-small:first-of-type { + border-top: var(--border-light); +} +.card--article-small::before { + content: ""; + width: 100%; + border-top: 1px solid transparent; + position: absolute; + top: -1px; + left: 0; +} +.card--article-small:hover { + background-color: var(--grey-950); + border-color: var(--color-txt); +} +.card--article-small:hover::before { + border-color: var(--color-txt); +} +.card--article-small .keywords { + display: none; } @media screen and (max-width: 1080px) { .card--article-small .title { @@ -812,65 +1429,240 @@ button:disabled { } @media screen and (max-width: 560px) { .card--article-small .content { - padding: 0; + display: contents; } .card--article-small .title { - margin-bottom: 0; - } - .card--article-small time { font-size: var(--fs-small); - margin-top: 0.25em; + padding-top: calc(var(--spacing) * 0.25); } - .card--article-small .keywords { - margin-top: 0.5em; - padding-bottom: 0; + .card--article-small figure { + grid-row: 1/3; + } + .card--article-small .description { + grid-column: span 2; } } .card--impact { - display: grid; - grid-gap: var(--padding-inner); - grid-template-columns: 2fr 3fr; - position: relative; - padding: calc(var(--spacing) * 0.5) 0; + container-type: inline-size; + container-name: impact; + border: var(--border-light); + margin-bottom: calc(var(--padding-body) * 1); + padding: var(--padding-inner); + display: flex; + flex-direction: column; + align-items: start; + justify-content: start; + gap: calc(var(--spacing) * 0.25); border-bottom: var(--border-light); + position: relative; } .card--impact:first-of-type { border-top: var(--border-light); } -.card--impact .content { - grid-column: 2; +.card--impact::before { + content: ""; + width: 100%; + border-top: 1px solid transparent; + position: absolute; + top: -1px; + left: 0; +} +.card--impact:hover { + background-color: var(--grey-950); + border-color: var(--color-txt); +} +.card--impact:hover::before { + border-color: var(--color-txt); +} +.card--impact > picture, .card--impact > figure { + aspect-ratio: 3/1; + overflow: hidden; +} +.card--impact > picture img, .card--impact > figure img { + width: 100%; + height: 100%; + -o-object-fit: cover; + object-fit: cover; +} +.card--impact .pin { + z-index: 90; + width: 18px; + height: 18px; + transform: rotate(45deg); + transform-origin: center; +} +.card--impact .pin svg { + width: 100%; + height: 100%; + fill: var(--color-txt); +} +.card--impact .title { + font-size: var(--fs-normal); + font-weight: 500; + text-transform: uppercase; + line-height: var(--leading-title); + padding-top: calc(var(--spacing) * 0.5); + margin-right: 2ch; +} +.card--impact .date { + font-size: var(--fs-small); + margin-top: calc(var(--spacing) * 1.5); +} +.card--impact .investigations { + text-decoration: none; + list-style: none; +} +.card--impact .investigations li { + font-size: var(--fs-small); + color: var(--color-txt-light); +} +.card--impact .investigations li a { + text-decoration: none; +} +.card--impact .investigations li a::before { + content: "↪ "; +} +.card--impact .card--open-graph { + width: 100%; + margin: calc(var(--spacing) * 0.5) 0; +} +.card--impact .pin { + position: absolute; + top: var(--padding-inner); + right: var(--padding-inner); +} +.card--impact > figure { + width: calc(100% + var(--padding-inner) * 2); + position: relative; + left: calc(var(--padding-inner) * -1); + top: calc(var(--padding-inner) * -1); +} +.card--impact .tag { + position: relative; + top: 3px; +} + +@media screen and (min-width: 768px) { + .card--impact, + .grid-sizer { + width: calc(50% - 13px); + } +} +.card--impact-small { + border-bottom: var(--border-light); + display: grid; + grid-gap: var(--padding-inner); + grid-template-columns: 2fr 3fr; + position: relative; + padding: var(--padding-inner) 0; + position: relative; +} +.card--impact-small:first-of-type { + border-top: var(--border-light); +} +.card--impact-small.has-link { + border-bottom: var(--border-light); + position: relative; +} +.card--impact-small.has-link:first-of-type { + border-top: var(--border-light); +} +.card--impact-small.has-link::before { + content: ""; + width: 100%; + border-top: 1px solid transparent; + position: absolute; + top: -1px; + left: 0; +} +.card--impact-small.has-link:hover { + background-color: var(--grey-950); + border-color: var(--color-txt); +} +.card--impact-small.has-link:hover::before { + border-color: var(--color-txt); +} +.card--impact-small .btn--go-to { + position: absolute; + right: var(--padding-inner); + bottom: calc(var(--padding-inner) - 3px); +} +.card--impact-small .btn--go-to svg { + width: 15px; + height: 15px; + fill: var(--color-txt); +} +@media screen and (max-width: 768px) { + .card--impact-small .btn--go-to svg { + width: 11px; + height: 11px; + } } @media screen and (max-width: 560px) { - .card--impact:not([data-impact-type=media]) .content { + .card--impact-small .btn--go-to { + right: calc(var(--padding-inner) * 0.5); + bottom: calc(var(--padding-inner) * 0.25); + } + .card--impact-small .btn--go-to svg { + width: 11px; + height: 11px; + } +} +.card--impact-small:hover .btn--go-to { + animation: wiggle-left 0.8s ease-in-out; +} +.card--impact-small .btn--go-to { + top: calc(var(--padding-inner) * 1); + bottom: auto; +} +.card--impact-small .content { + grid-column: 2; +} +.card--impact-small .card--open-graph { + grid-column: 2; + z-index: 10; +} +.card--impact-small .keywords { + grid-column: 2; + z-index: 10; +} +@media screen and (max-width: 560px) { + .card--impact-small .keywords { + display: none; + } +} +@media screen and (max-width: 560px) { + .card--impact-small:not([data-impact-type=media]) .content, + .card--impact-small .keywords { grid-column: span 2; } } -.card--impact .tag { +.card--impact-small .tag { width: auto; justify-self: start; position: relative; top: -5px; } -.card--impact .content .see-more { - color: var(--color-txt-light); - text-decoration: none; - white-space: nowrap; +.card--impact-small:not([data-impact-type=media]) .content { + padding-right: calc(var(--padding-inner) * 2.5); } -.card--impact .content .see-more::after { - content: " +"; +@media screen and (max-width: 560px) { + .card--impact-small:not([data-impact-type=media]) .content { + padding-right: 0; + } } -.card--impact .open-graph__details { - grid-column: span 2; +.card--impact-small .open-graph__details { + grid-column: 2; } -.card--impact .open-graph__details summary, -.card--impact .open-graph__details .summary-inner { +.card--impact-small .open-graph__details summary, +.card--impact-small .open-graph__details .summary-inner { display: flex; align-items: center; cursor: pointer; gap: 0.5ch; } -.card--impact .open-graph__details .arrow-details { +.card--impact-small .open-graph__details .arrow-details { line-height: 0; --size: 11px; height: var(--size); @@ -881,69 +1673,256 @@ button:disabled { position: relative; top: -2px; } -.card--impact .open-graph__details .arrow-details svg { +.card--impact-small .open-graph__details .arrow-details svg { transition: transform 0.2s ease-in; width: 100%; fill: var(--color-txt); } -.card--impact .open-graph__details[open] .arrow-details svg { +.card--impact-small .open-graph__details summary:hover { + color: var(--color-txt); +} +.card--impact-small .open-graph__details summary:hover .arrow-details svg { + fill: var(--color-txt); +} +.card--impact-small .open-graph__details[open] .arrow-details svg { transform: rotate(90deg); } -.card--impact .open-graph__details summary { +.card--impact-small .open-graph__details summary { color: var(--color-txt-light); } -.card--impact .open-graph__details summary .arrow-details svg { +.card--impact-small .open-graph__details summary .arrow-details svg { fill: var(--color-txt-light); } -.card--impact .open-graph__details summary { - display: grid; - grid-gap: var(--padding-inner); - grid-template-columns: 2fr 3fr; - position: relative; -} -.card--impact .open-graph__details summary .summary-inner { - grid-column: 2; -} -.card--impact .open-graph__inner { - margin-top: calc(var(--spacing) * 0.5); - margin-bottom: calc(var(--spacing) * 1); -} -@media screen and (min-width: 768px) { - .card--impact .open-graph__inner { - margin-bottom: calc(var(--spacing) * 2); +@media screen and (max-width: 560px) { + .card--impact-small .open-graph__details { + grid-column: 1/3; + } + .card--impact-small .open-graph__details summary { + display: grid; + grid-gap: var(--padding-inner); + grid-template-columns: 2fr 3fr; + position: relative; + grid-row-gap: 0; + } + .card--impact-small .open-graph__details summary .summary-inner { + grid-column: 2; } } -.card--impact .open-graph__inner .card--open-graph { - margin-bottom: calc(var(--spacing) * 0.5); -} -.card--open-graph { +.card--package { + position: relative; + max-width: var(--max-w-cards); + border: var(--border-light); + padding: var(--padding-inner); display: grid; grid-gap: var(--padding-inner); grid-template-columns: 2fr 3fr; position: relative; - border: var(--border-light); - border-radius: var(--radius-small); + container-type: inline-size; + container-name: cardpackage; position: relative; } -.card--open-graph figure { - grid-column: 1; - grid-row: 1; -} -.card--open-graph figure { +.card--package > picture, .card--package > figure { aspect-ratio: 16/9; display: flex; overflow: hidden; } -.card--open-graph figure img { +.card--package > picture img, .card--package > figure img { width: 100%; height: 100%; -o-object-fit: cover; object-fit: cover; transition: cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.5s; } -.card--open-graph:hover figure img { - transform: scale(1.05); +.card--package figure { + border-radius: var(--radius-small); + background-color: var(--color-accent); +} +.card--package figure img { + opacity: 0.8; + filter: grayscale(1); +} +.card--package .content { + display: flex; + flex-direction: column; + font-size: var(--fs-small); +} +.card--package .title { + font-weight: 500; + font-size: var(--fs-normal); + line-height: var(--leading-title); + margin-bottom: 0.75em; + text-wrap: balance; + max-width: 42ch; + text-transform: uppercase; + padding-top: calc(var(--spacing) * 0.25); +} +.card--package .title a { + text-decoration: none; +} +@media screen and (max-width: 768px) { + .card--package .title { + font-size: var(--fs-normal); + } +} +.card--package .title .icon { + padding-right: 1ch; + position: relative; + top: 1px; +} +.card--package .title .icon svg { + height: 15px; + width: 15px; + fill: var(--color-txt); +} +.card--package .short { + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + flex-grow: 1; + padding-right: calc(var(--padding-inner) * 2); +} +.card--package ul { + display: flex; + list-style: none; + gap: 1ch; + color: var(--color-txt-light); + padding-top: calc(var(--spacing) * 0.5); +} +.card--package ul li + li::before { + content: "|"; + padding-right: 1ch; +} +@media screen and (max-width: 768px) { + .card--package ul { + font-size: var(--fs-small); + } +} +.card--package .btn--go-to { + position: absolute; + right: calc(var(--padding-inner) * 1); + bottom: var(--padding-inner); +} +.card--package .btn--go-to { + position: absolute; + right: var(--padding-inner); + bottom: calc(var(--padding-inner) - 3px); +} +.card--package .btn--go-to svg { + width: 15px; + height: 15px; + fill: var(--color-txt); +} +@media screen and (max-width: 768px) { + .card--package .btn--go-to svg { + width: 11px; + height: 11px; + } +} +@media screen and (max-width: 560px) { + .card--package .btn--go-to { + right: calc(var(--padding-inner) * 0.5); + bottom: calc(var(--padding-inner) * 0.25); + } + .card--package .btn--go-to svg { + width: 11px; + height: 11px; + } +} +.card--package:hover .btn--go-to { + animation: wiggle-left 0.8s ease-in-out; +} +.card--package:hover { + background-color: var(--grey-950); + border-color: var(--color-txt); +} + +[data-template=investigation-summary] .card--package { + border: none; + padding-left: 0; + padding-right: 0; + border-bottom: var(--border-light); +} +[data-template=investigation-summary] .card--package:first-of-type { + border-top: var(--border-light); +} +[data-template=investigation-summary] .card--package.has-link { + border-bottom: var(--border-light); + position: relative; +} +[data-template=investigation-summary] .card--package.has-link:first-of-type { + border-top: var(--border-light); +} +[data-template=investigation-summary] .card--package.has-link::before { + content: ""; + width: 100%; + border-top: 1px solid transparent; + position: absolute; + top: -1px; + left: 0; +} +[data-template=investigation-summary] .card--package.has-link:hover { + background-color: var(--grey-950); + border-color: var(--color-txt); +} +[data-template=investigation-summary] .card--package.has-link:hover::before { + border-color: var(--color-txt); +} +@media screen and (max-width: 560px) { + [data-template=investigation-summary] .card--package figure { + margin-left: 0px; + } +} +[data-template=investigation-summary] .card--package .title { + font-size: var(--fs-small); +} + +@media screen and (max-width: 560px) { + .card--package .content { + display: contents; + } + .card--package figure { + grid-row: 1/3; + } + .card--package .title { + font-size: var(--fs-small); + padding-top: calc(var(--spacing) * 0.25); + margin-bottom: 0; + } + .card--package .short { + grid-column: span 2; + } + .card--package ul { + grid-column: 2; + grid-row: 2; + padding-top: 0; + } +} +.card--open-graph { + container-type: inline-size; + container-name: opengraph; + border: 1px solid var(--grey-600); + border-radius: var(--radius-small); + position: relative; +} +.card--open-graph .open-graph__inner { + display: grid; + grid-gap: var(--padding-inner); + grid-template-columns: 2fr 3fr; + position: relative; +} +.card--open-graph figure { + grid-column: 1; + grid-row: 1; + width: 100%; + height: 100%; +} +.card--open-graph figure img { + width: 100%; + height: 100%; + -o-object-fit: cover; + object-fit: cover; } @media screen and (max-width: 560px) { .card--open-graph figure { @@ -952,18 +1931,22 @@ button:disabled { } } .card--open-graph .content { - padding: var(--padding-inner); - padding-left: 0; + padding-top: calc(var(--padding-inner) * 0.75); + padding-bottom: calc(var(--padding-inner) * 0.75); + padding-right: var(--padding-inner); grid-column: 2; grid-row: 1; } .card--open-graph .site-name { color: var(--color-txt-light); - font-size: var(--fs-small); + font-size: var(--fs-xsmall); + margin-bottom: 3px; } .card--open-graph .title { - font-size: var(--fs-normal); - font-weight: normal; + font-size: var(--fs-small); + font-weight: 500; + line-height: 1.1; + padding-top: 2px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; @@ -974,31 +1957,163 @@ button:disabled { text-decoration: none; } .card--open-graph .description { - font-size: var(--fs-small); + font-size: var(--fs-xsmall); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; } -.card--open-graph:hover .title { - text-decoration: underline; +.card--open-graph:hover { + border-color: var(--color-txt); + background-color: var(--grey-950); } -@keyframes add-border { - from { - border-bottom-color: transparent; +@container opengraph (width < 500px) {} +.swiper { + --slide-padding: 30px; +} +.swiper .swiper-button-prev, +.swiper .swiper-button-next { + --swiper-navigation-size: 32px; + color: var(--color-txt); + background-color: var(--color-bg); + height: 100%; + width: var(--slide-padding); + top: 0px !important; + height: calc(100% - var(--spacing) * 1); +} +.swiper .swiper-button-prev svg, +.swiper .swiper-button-next svg { + width: 14px; +} +.swiper .swiper-button-prev.swiper-button-disabled, +.swiper .swiper-button-next.swiper-button-disabled { + opacity: 1; +} +.swiper .swiper-button-prev.swiper-button-disabled svg, +.swiper .swiper-button-next.swiper-button-disabled svg { + opacity: 0.05; +} +.swiper .swiper-button-prev { + left: 0px !important; + top: 0px; + justify-content: flex-start; +} +.swiper .swiper-button-next { + right: 0px !important; + top: 0px; + justify-content: flex-end; +} +.swiper .swiper-slide { + padding-left: var(--slide-padding); + padding-right: var(--slide-padding); +} +.swiper .swiper-pagination { + position: relative; + margin-top: 0px !important; + margin-top: calc(var(--spacing) * 0.5) !important; +} +.swiper .swiper-pagination .swiper-pagination-bullet { + width: 15px; + height: 4px; + border-radius: 2px; + background: var(--color-txt-light); +} +.swiper .swiper-pagination .swiper-pagination-bullet-active { + background: var(--color-txt); +} +@media screen and (max-width: 560px) { + .swiper .swiper-button-prev, + .swiper .swiper-button-next { + display: none; } - to { - border-bottom: var(--grey-800); + .swiper .swiper-slide { + padding: 0px; } } + +.slider-before-after { + width: 100%; + max-width: 700px; + z-index: 300; + display: grid; + place-content: center; + position: relative; + overflow: hidden; + --position: 50%; +} +.slider-before-after img { + display: block; + max-width: 100%; +} +.slider-before-after .image-container { + position: relative; + width: 100%; +} +.slider-before-after .slider-image { + width: 100%; + height: 100%; + -o-object-fit: cover; + object-fit: cover; + -o-object-position: left; + object-position: left; +} +.slider-before-after .image-before { + position: absolute; + inset: 0; + width: var(--position); +} +.slider-before-after .slider { + position: absolute; + inset: 0; + cursor: pointer; + opacity: 0; + /* for Firefox */ + width: 100%; + height: 100%; +} +.slider-before-after .slider:focus-visible ~ .slider-button { + outline: 5px solid black; + outline-offset: 3px; +} +.slider-before-after .slider-line { + position: absolute; + inset: 0; + width: 0.2rem; + height: 100%; + background-color: #fff; + /* z-index: 10; */ + left: var(--position); + transform: translateX(-50%); + pointer-events: none; +} +.slider-before-after .slider-button { + position: absolute; + background-color: #fff; + color: black; + padding: 0.5rem; + border-radius: 100vw; + display: grid; + place-items: center; + top: 50%; + left: var(--position); + transform: translate(-50%, -50%); + pointer-events: none; + /* z-index: 100; */ + box-shadow: 1px 1px 1px hsla(0, 50%, 2%, 0.5); +} + #site-header { - z-index: 900; + z-index: var(--z-header); --gap: 3ch; + position: fixed; + top: 0; + left: 0; width: 100vw; height: var(--header-h); background-color: var(--color-bg); padding: 0 var(--padding-body); + box-shadow: -1px 4px 10px 0px var(--color-bg); } #site-header .site-header__inner { width: 100%; @@ -1025,7 +2140,7 @@ button:disabled { width: 80px; } } -#site-header .title-page { +#site-header .header__title-page { display: none; flex-grow: 2; text-align: left; @@ -1042,6 +2157,7 @@ button:disabled { align-items: center; gap: var(--gap); text-transform: uppercase; + font-weight: 500; } #site-header #theme-toggle { width: var(--h-block); @@ -1059,6 +2175,9 @@ button:disabled { display: flex; gap: 1ch; } +#site-header #lang-toggle button { + font-weight: 500; +} #site-header #lang-toggle button:disabled { color: var(--color-txt-light); } @@ -1067,6 +2186,7 @@ button:disabled { } #site-header #menu-toggle svg { width: 30px; + fill: var(--color-txt); } #site-header #menu-toggle .close { display: none; @@ -1075,39 +2195,6 @@ button:disabled { fill: var(--grey-200) !important; } -#site-header.is-visible { - position: fixed; - top: 0; -} -#site-header.is-visible .site-header__inner { - border-bottom: var(--border-light); -} -#site-header.is-visible .site-header__inner #nav-highlight li:not(.soutenir) { - display: none; -} -@media screen and (min-width: 768px) { - #site-header.is-visible .site-header__inner #site-title { - flex-grow: 0; - width: calc((100vw - var(--max-w-content)) / 2 - var(--padding-body) * 2); - } -} -@media screen and (max-width: 1080px) { - #site-header.is-visible .site-header__inner #site-title { - width: calc(var(--banner-medium) - var(--padding-body)); - } -} -#site-header.is-visible .site-header__inner .title-page { - flex-grow: 2; -} -@media screen and (min-width: 768px) { - #site-header.is-visible .site-header__inner .title-page { - display: block; - } -} -#site-header.is-visible ~ main { - margin-top: var(--header-h); -} - @media screen and (max-width: 1080px) { #nav-highlight { display: none; @@ -1124,61 +2211,24 @@ button:disabled { background-color: var(--color-bg); border-left: var(--border-light); padding: var(--padding-body); - z-index: 1000; + z-index: calc(var(--z-header) - 1); display: flex; flex-direction: column; } +@media screen and (max-width: 768px) { + #site-menu { + transition: right 0.4s ease-in; + } +} @media screen and (max-width: 560px) { #site-menu { width: 100vw; right: -100vw; } } -#site-menu form { +#site-menu .search-form { margin-top: calc(var(--spacing) * 1); margin-bottom: calc(var(--spacing) * 1); - --icon: 40px; - display: grid; - grid-template-columns: var(--icon) 1fr; -} -#site-menu form input { - grid-column: 1/3; - grid-row: 1; - height: calc(var(--h-block) * 1.5); - width: 100%; - background: var(--color-bg); - border: 1px solid var(--color-txt); - padding-left: var(--icon); - font-family: var(--font); - font-size: var(--fs-normal); - color: var(--color-txt); - padding-top: 3px; -} -#site-menu form input::-moz-placeholder { - font-family: var(--font); - font-size: var(--fs-normal); - color: var(--color-txt); -} -#site-menu form input::placeholder { - font-family: var(--font); - font-size: var(--fs-normal); - color: var(--color-txt); -} -#site-menu form input:focus { - border-color: var(--color-accent); - outline: none; -} -#site-menu form .icon { - grid-column: 1; - grid-row: 1; - z-index: 10; - width: 100%; - display: flex; - align-items: center; - justify-content: center; -} -#site-menu form .icon svg { - width: 20px; } #site-menu nav { flex-grow: 1; @@ -1186,6 +2236,7 @@ button:disabled { #site-menu nav ul { list-style-type: none; text-transform: uppercase; + font-weight: 500; } #site-menu nav ul li { font-size: var(--fs-normal); @@ -1201,14 +2252,10 @@ button:disabled { #site-menu nav ul .highlight { text-transform: uppercase; } +#site-menu .socials { + padding-bottom: calc(var(--spacing) * 0.75); +} -body.menu-open { - overflow: hidden; -} -body.menu-open main, body.menu-open footer, body.menu-open #nav-highlight { - transition: opacity 0.3s ease-in; - opacity: 0.1; -} body.menu-open #site-menu { right: 0; } @@ -1222,6 +2269,12 @@ body.menu-open #menu-toggle .close { #site-footer { background-color: var(--dark); padding: calc(var(--padding-body) * 2) var(--padding-body); + z-index: 500; +} +#site-footer .site-footer__container { + max-width: var(--max-w-cards); + margin: 0 auto; + padding: 0 var(--padding-body); } #site-footer .logo { margin-top: calc(var(--spacing) * 0.25); @@ -1232,6 +2285,7 @@ body.menu-open #menu-toggle .close { } #site-footer p { margin: calc(var(--spacing) * 0.5) 0; + font-size: var(--fs-small); } #site-footer p a { text-decoration: none; @@ -1245,13 +2299,13 @@ body.menu-open #menu-toggle .close { color: var(--grey-600); } #site-footer .footer__mentions p { - font-size: var(--fs-small); + font-size: var(--fs-xsmall); color: var(--color-txt); } +#site-footer .footer__socials > p { + font-weight: 500; +} @media screen and (max-width: 768px) { - #site-footer { - margin-top: calc(var(--spacing) * 2); - } #site-footer .footer__socials { margin-top: calc(var(--spacing) * 0.75); padding-top: calc(var(--spacing) * 0.25); @@ -1266,21 +2320,29 @@ body.menu-open #menu-toggle .close { padding-top: calc(var(--spacing) * 0.25); } #site-footer .footer__mentions p { - font-size: var(--fs-small); + font-size: 12px; text-align: center; } + #site-footer .p__small { + font-size: 12px; + } } @media screen and (min-width: 768px) { #site-footer .site-footer__container { display: grid; - grid-template-columns: 1fr 1fr; + grid-template-columns: 3fr 2fr; -moz-column-gap: calc(var(--spacing) * 2); column-gap: calc(var(--spacing) * 2); - max-width: 1200px; - margin: 0 auto; + } + #site-footer .footer__newsletter { + grid-column: 1; + grid-row: 2; + } + #site-footer .footer__socials { + grid-column: 2; + grid-row: 2; } #site-footer .footer__socials .socials { - max-width: 400px; -moz-columns: 2; columns: 2; margin-top: calc(var(--spacing) * 1); @@ -1296,92 +2358,267 @@ body.menu-open #menu-toggle .close { body { min-height: 100dvh; min-height: 100vh; + width: 100vw; + overflow-x: hidden; display: flex; flex-direction: column; } body main { flex-grow: 1; padding: 0 var(--padding-body); + padding-top: var(--header-h); + padding-bottom: calc(var(--spacing) * 2); + min-height: 100dvh; + min-height: 100vh; +} +body main .page__header, +body main .page__content { + max-width: var(--max-w-cards); + margin-inline: auto; } -[data-template=investigations] main #container-cards { - max-width: var(--max-w-container); +.page__type { + height: calc(var(--h-block) * 0.75); + border-radius: var(--radius-small); + border: var(--border-medium); + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 1.5ch; + padding-top: 3px; + font-size: var(--fs-xsmall); + background-color: var(--color-bg); + color: var(--color-txt-light); + font-weight: 500; + overflow: hidden; + white-space: nowrap; + text-transform: uppercase; +} + +main .page__header { + margin-top: calc(var(--spacing) * 3); + margin-bottom: calc(var(--spacing) * 2); +} +@media screen and (max-width: 768px) { + main .page__header { + margin-top: calc(var(--spacing) * 2); + } +} +main .page__header .page__title { + max-width: var(--max-w-content); + text-transform: uppercase; + font-weight: normal; + font-size: var(--fs-medium); + font-weight: 500; + line-height: var(--leading-title); + margin-top: calc(var(--spacing) * 1); + margin-bottom: calc(var(--spacing) * 0.5); + text-wrap: balance; +} +main .page__header .description-medium { + max-width: 58ch; + line-height: 1.1; +} + +.container-cards { + max-width: var(--max-w-cards); margin: 0 auto; display: grid; - grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); - grid-auto-rows: minmax(100px, auto); - grid-gap: var(--padding-body); - margin-bottom: 10vh; + grid-gap: calc(var(--spacing) * 1); +} + +@media screen and (min-width: 560px) { + .container-cards__investigations { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); + grid-auto-rows: minmax(100px, auto); + grid-gap: var(--padding-body); + margin-bottom: 10vh; + } +} +@media screen and (max-width: 560px) { + .container-cards__investigations { + margin-bottom: 10vh; + } +} + +@media screen and (min-width: 768px) { + .section--home { + border-bottom: var(--border-light); + } +} +@media screen and (max-width: 1080px) { + .section--home { + padding-bottom: calc(var(--spacing) * 4); + margin-bottom: calc(var(--spacing) * 4); + } + .section--home:first-of-type { + padding-top: calc(var(--spacing) * 2); + } + .section--home .col-left { + margin-bottom: calc(var(--spacing) * 3); + } +} +.section--home .baseline-section { + max-width: 42ch; + font-size: var(--fs-medium); + line-height: 1.1; + margin-bottom: calc(var(--spacing) * 1); +} +.section--home .btn--bold-inline { + text-transform: none; + font-weight: 500; + font-size: var(--fs-small); +} +.section--home .btn--bold-inline .icon { + position: relative; + top: 2px; +} +.section--home .btn--bold-inline svg { + width: 13px; + height: 13px; +} +@media screen and (min-width: 1080px) { + .section--home .section--inner { + max-width: 1600px; + margin-inline: auto; + margin-bottom: calc(var(--spacing) * 4); + padding-top: calc(var(--spacing) * 4); + display: grid; + --gap: calc(var(--padding-body)*2); + grid-template-columns: 1fr 2fr; + grid-gap: var(--gap); + } + .section--home .col-left { + align-self: start; + position: sticky; + top: calc(var(--header-h) + var(--spacing) * 4); + } + .section--home .col-right { + width: 100%; + height: 100%; + } +} +@media screen and (max-width: 768px) { + .section--home .card--article, + .section--home .card--impact { + margin-bottom: calc(var(--spacing) * 1); + } + .section--home .baseline-section { + font-size: var(--fs-big); + } +} + +@media screen and (min-width: 768px) { + #home__investigations .col-right { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--padding-body); + } + #home__investigations .col-right .card--article:first-of-type { + grid-column: span 2; + display: grid; + grid-template-columns: 1fr 1fr; + } + #home__investigations .col-right .card--article:first-of-type figure, + #home__investigations .col-right .card--article:first-of-type picture { + grid-column: span 2; + } + #home__investigations .col-right .card--article:first-of-type .title { + grid-column: span 2; + font-size: var(--fs-medium); + padding-right: 3ch; + margin-bottom: calc(var(--spacing) * 1); + } + #home__investigations .col-right .card--article:first-of-type .description { + grid-column: 1; + grid-row: 3; + padding-right: 3ch; + display: block; + -webkit-line-clamp: unset; + -webkit-box-orient: unset; + overflow: visible; + } + #home__investigations .col-right .card--article:first-of-type dl { + grid-column: 2; + grid-row: 3; + } + #home__investigations .col-right .card--article:first-of-type .keywords-wrapper { + grid-column: 2; + grid-row: 4; + } + #home__investigations .col-right .see-more { + grid-column: span 2; + } } [data-template=investigation-summary] main { position: relative; } -[data-template=investigation-summary] main header .page-type { - text-transform: uppercase; - color: var(--color-txt-light); - margin-bottom: calc(var(--spacing) * 0.5); +[data-template=investigation-summary] main .page__header { + margin-inline: auto; + max-width: var(--max-w-content); } -[data-template=investigation-summary] main header h2 { - font-size: var(--fs-xbig); - line-height: var(--leading-tight); +[data-template=investigation-summary] main .panel-left { + width: calc((100vw - var(--max-w-cards) - var(--padding-body) * 4) * 0.5); +} +@media screen and (max-width: 1340px) { + [data-template=investigation-summary] main { + margin-left: auto; + margin-right: calc(var(--padding-body) * 3); + } + [data-template=investigation-summary] main .panel-left { + width: calc(100vw - var(--max-w-cards) - var(--padding-body) * 6); + } +} +@media screen and (max-width: 1220px) { + [data-template=investigation-summary] main { + margin-left: auto; + margin-right: 0px; + width: calc(100% - var(--panel-w) * 0.5 - var(--padding-body)); + } + [data-template=investigation-summary] main .panel-left { + width: calc(var(--panel-w) * 0.5); + } +} +[data-template=investigation-summary] main .section__article { + margin-top: calc(var(--spacing) * 3); + margin-bottom: calc(var(--spacing) * 3); + max-width: var(--max-w-content); + margin-inline: auto; +} +[data-template=investigation-summary] main .section__article:target { + padding-top: calc(var(--header-h) + var(--spacing) * 1); } [data-template=investigation-summary] main .section__article a:hover { color: var(--grey-200); } [data-template=investigation-summary] main .section__article .section__title { - font-weight: normal; + font-weight: 500; text-transform: uppercase; - margin-bottom: calc(var(--spacing) * 0.5); + margin-bottom: calc(var(--spacing) * 1); + padding-right: 2ch; + text-wrap: balance; } -[data-template=investigation-summary] main #hero { - width: 100vw; - position: relative; - left: calc(var(--padding-body) * -1); -} -[data-template=investigation-summary] main #hero figcaption { - color: var(--color-txt-light); +[data-template=investigation-summary] main #section__dl, +[data-template=investigation-summary] main #section__impacts, +[data-template=investigation-summary] main #section__package, +[data-template=investigation-summary] main #section__related-articles { font-size: var(--fs-small); - padding-top: calc(var(--spacing) * 0.5); -} -@media screen and (max-width: 560px) { - [data-template=investigation-summary] main #hero figcaption { - font-size: var(--fs-xsmall); - } -} -[data-template=investigation-summary] main #hero.hero-video figure { - width: 100%; -} -[data-template=investigation-summary] main #hero.hero-video figure img { - width: 100%; - aspect-ratio: 16/9; - -o-object-fit: cover; - object-fit: cover; -} -[data-template=investigation-summary] main #nav--page ul { - list-style: none; -} -[data-template=investigation-summary] main #nav--page ul li { - text-align: center; - color: var(--color-txt-light); -} -[data-template=investigation-summary] main #nav--page ul li a { - display: block; - padding: 0.3em 0; - text-decoration: none; -} -[data-template=investigation-summary] main #section__short { - font-size: var(--fs-medium); } [data-template=investigation-summary] main #section__dl { margin-top: calc(var(--spacing) * 1.5); border-bottom: var(--border-light); + max-width: var(--max-w-content); + margin-inline: auto; } [data-template=investigation-summary] main #section__dl .dl__group { display: grid; grid-gap: var(--padding-inner); grid-template-columns: 2fr 3fr; position: relative; + -moz-column-gap: 1ch; + column-gap: 1ch; border-top: var(--border-light); padding: calc(var(--spacing) * 0.5) 0; } @@ -1395,173 +2632,519 @@ body main { [data-template=investigation-summary] main #section__dl ul:not(.keywords) li { padding-bottom: 0.2em; } +[data-template=investigation-summary] main #section__synthese { + max-width: var(--max-w-content); + margin-inline: auto; +} [data-template=investigation-summary] main #section__synthese p + p { - margin-top: 0.5em; + margin-top: 1em; } [data-template=investigation-summary] main #section__synthese h4 { margin-top: 2em; margin-bottom: 1em; font-size: var(--fs-normal); - font-weight: normal; + font-weight: 500; -webkit-text-decoration: 1px underline var(--color-txt-light); text-decoration: 1px underline var(--color-txt-light); text-underline-offset: 3px; } - -#share-banner__desktop { - display: none; +[data-template=investigation-summary] main .panel-left { + height: calc(100vh - var(--header-h)); + position: fixed; + left: var(--padding-body); + padding-bottom: calc(var(--padding-body) * 1); + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: center; + z-index: calc(var(--z-header) - 1); } - -#share-banner__desktop ~ .modal--share { - opacity: 0; - pointer-events: none; - transition: opacity 0.2s ease-in; +[data-template=investigation-summary] main .panel-left #nav--page { + padding-bottom: var(--spacing); + width: calc(var(--panel-w) * 0.5); } - -#share-banner__desktop:checked ~ .modal--share { - opacity: 1; - pointer-events: auto; +[data-template=investigation-summary] main .panel-left #nav--page ul { + list-style: none; +} +[data-template=investigation-summary] main .panel-left #nav--page ul li { + text-align: center; + color: var(--color-txt-light); + margin-bottom: 4px; + font-weight: 500; + font-size: var(--fs-small); +} +[data-template=investigation-summary] main .panel-left #nav--page ul li a { + display: block; + padding: 5px 1ch; + text-decoration: none; +} +[data-template=investigation-summary] main .panel-left .btn--group { + width: calc(var(--panel-w) * 0.5); + display: flex; + flex-direction: column; + gap: calc(var(--spacing) * 0.25); +} +[data-template=investigation-summary] main .panel-left .btn--group button, +[data-template=investigation-summary] main .panel-left .btn--group .dropdown { + flex-grow: 1; + width: 100%; } @media screen and (max-width: 768px) { - [data-template=investigation-summary] main header { - padding-top: calc(var(--spacing) * 1.5); + [data-template=investigation-summary] main { + width: 100%; } - [data-template=investigation-summary] main header .page-type { - font-size: var(--fs-small); + [data-template=investigation-summary] main header { + margin-bottom: calc(var(--spacing) * 1); } [data-template=investigation-summary] main .section__article { - margin: calc(var(--spacing) * 1.5) 0; + margin-top: calc(var(--spacing) * 2); + margin-bottom: calc(var(--spacing) * 2); } - [data-template=investigation-summary] main #section__impacts, [data-template=investigation-summary] main #section__en-lien { - margin-top: 0px; - } - [data-template=investigation-summary] main #hero { - margin: calc(var(--spacing) * 1.5) 0; - } - [data-template=investigation-summary] main #hero figcaption { - margin: 0 var(--padding-body); - } - [data-template=investigation-summary] main .modal--share { - position: absolute; - width: calc(100% - var(--padding-body) * 2); - bottom: calc(var(--spacing) * 2); - } - [data-template=investigation-summary] main #banner--page { - padding: calc(var(--spacing) * 0.5) 0; - position: fixed; - bottom: 0; - left: 0; - width: 100vw; - padding: calc(var(--spacing) * 0.75) var(--padding-body); - padding-top: var(--spacing); - background-color: var(--color-bg); - background: linear-gradient(0deg, var(--color-bg) 0%, var(--color-bg) 64%, transparent 100%); - z-index: 800; - } - [data-template=investigation-summary] main #banner--page #nav--page { + [data-template=investigation-summary] main .panel-left { display: none; } - [data-template=investigation-summary] main #banner--page .btn--group { - display: flex; - gap: calc(var(--spacing) * 0.25); - position: relative; - } - [data-template=investigation-summary] main #banner--page .btn--group > button, - [data-template=investigation-summary] main #banner--page .btn--group > label { - width: 50%; - cursor: pointer; - } - [data-template=investigation-summary] main #banner--page { - opacity: 0; - pointer-events: none; - transition: opacity 0.2s ease-in; - } - [data-template=investigation-summary] main #banner--page.is-visible { - opacity: 1; - pointer-events: auto; - } } -@media screen and (max-width: 560px) { - [data-template=investigation-summary] main #section__dl .dl__group { - -moz-column-gap: 1ch; - column-gap: 1ch; - font-size: var(--fs-small); - padding: calc(var(--spacing) * 0.25) 0; +@media screen and (max-width: 768px) { + [data-template=report] #toggle-panel { + display: none; + } + [data-template=report] #report__aside { + width: 100vw; + position: fixed; + top: 0px; + height: 100vh; + left: -100vw; + z-index: calc(var(--z-header) + 10); + transition: left 0.4s ease-in; + background: var(--color-bg); + } + [data-template=report] #report__aside .panel__header { + margin: 0 var(--padding-body); + border-bottom: var(--border-aside); + height: var(--header-h); + } + [data-template=report] #report__aside .panel__content { + height: calc(100dvh - var(--header-h)); + height: calc(100vh - var(--header-h)); + padding-bottom: 30vh; + } + [data-template=report] #report__aside .btn--group { + display: none; + } + [data-template=report] .panel-open #report__aside { + left: 0px; } } @media screen and (min-width: 768px) { - [data-template=investigation-summary] main #banner--page { - height: calc(100vh - var(--header-h)); - height: calc(100dvh - var(--header-h)); - margin-bottom: calc((100vh - var(--header-h)) * -1); - margin-bottom: calc((100dvh - var(--header-h)) * -1); - padding: var(--padding-body); - padding-left: 0px; - position: sticky; + [data-template=report] #toggle-panel { + position: fixed; top: var(--header-h); - width: var(--banner-medium); - display: flex; - flex-direction: column; - justify-content: space-between; + left: var(--padding-body); + z-index: calc(var(--z-header) + 100); } - [data-template=investigation-summary] main #banner--page .btn--group { - display: flex; - flex-direction: column; - align-items: center; - gap: calc(var(--spacing) * 0.25); + [data-template=report] #report__aside { + position: fixed; + top: var(--header-h); + left: var(--padding-body); + width: var(--panel-w); + height: calc(100vh - var(--header-h)); + z-index: calc(var(--z-header) + 200); + background-color: var(--color-bg); + box-shadow: 4px 0px 4px 1px var(--color-bg); } - [data-template=investigation-summary] main #banner--page .btn--group > button, - [data-template=investigation-summary] main #banner--page .btn--group > label { - width: 100%; - max-width: 160px; - cursor: pointer; + [data-template=report] #report__aside .panel__header { + border: var(--border-aside); + height: calc(var(--h-block) * 1.5); } - [data-template=investigation-summary] main #hero { - margin: calc(var(--spacing) * 3) 0; + [data-template=report] #report__aside .panel__content { + height: calc(100% - var(--h-block) * 4.25); + padding-bottom: 80px; } - [data-template=investigation-summary] main .section__article { - margin-left: var(--banner-medium); - margin-top: calc(var(--spacing) * 3); + [data-template=report] #report__aside { + left: calc(var(--panel-w) * -1); + transition: left ease-in-out 0.5s; + } + [data-template=report] #report { + transition: padding-left ease-in-out 0.5s; + } + [data-template=report] .panel-open #report__aside { + left: var(--padding-body); + } + [data-template=report] .panel-open #report { + padding-left: calc(var(--panel-w) + var(--padding-body) * 1); + } +} +.report__header { + max-width: var(--max-w-cards); + margin-inline: auto; + margin-top: calc(var(--spacing) * 3.25); + margin-bottom: calc(var(--spacing) * 0.5); + display: grid; + -moz-column-gap: var(--padding-inner); + column-gap: var(--padding-inner); + grid-template-columns: 65% 35%; + grid-template-rows: auto auto 1fr; + position: relative; + padding: var(--padding-body); + border: 1px solid var(--color-txt); + border-radius: var(--radius-small); +} +.report__header .report__title-group { + grid-row: 1; + grid-column: span 2; + margin-bottom: calc(var(--spacing) * 1); +} +.report__header .report__title-group .title { + text-transform: uppercase; + font-weight: normal; + font-size: var(--fs-medium); + line-height: var(--leading-tight); + font-weight: 500; + margin-top: calc(var(--spacing) * 1); + text-wrap: balance; +} +.report__header .report__title-group .subtitle { + font-size: var(--fs-medium); + line-height: var(--leading-tight); + font-weight: 500; + text-wrap: balance; +} +.report__header > picture, .report__header > figure { + aspect-ratio: 16/9; + display: flex; + overflow: hidden; +} +.report__header > picture img, .report__header > figure img { + width: 100%; + height: 100%; + -o-object-fit: cover; + object-fit: cover; + transition: cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.5s; +} +.report__header figure { + grid-row: 2; + grid-column: 2; +} +.report__header .report__dl { + grid-row: 2; + grid-column: 1; + align-self: end; + font-size: var(--fs-small); + border-bottom: var(--border-light); + align-items: flex-start; +} +.report__header .report__dl .dl__group { + display: grid; + grid-gap: var(--padding-inner); + grid-template-columns: 2fr 3fr; + position: relative; + border-top: var(--border-light); + padding: calc(var(--spacing) * 0.5) 0; +} +.report__header .report__dl dt { + color: var(--color-txt-light); + padding-right: 1ch; +} + +.report__btns { + max-width: var(--max-w-cards); + margin-inline: auto; + display: flex; + justify-content: start; + flex-wrap: wrap; + align-items: start; + gap: calc(var(--spacing) * 0.25); +} + +.report__content { + margin-top: calc(var(--spacing) * 4); +} +.report__content .section-content { + padding-left: calc(var(--padding-body) * 1.5); + padding-right: calc(var(--padding-body) * 1.5); + margin-bottom: calc(var(--spacing) * 6); +} +.report__content .section-content:target { + padding-top: calc(var(--header-h) + var(--spacing) * 2); +} +@media screen and (max-width: 768px) { + .report__content .section-content:target { + padding-top: calc(var(--header-h) + var(--spacing) * 0.5); + } +} +.report__content .section-title { + max-width: var(--max-w-content); + margin-inline: auto; + margin-bottom: calc(var(--spacing) * 2); + font-size: var(--fs-medium); + font-weight: 500; + text-wrap: balance; + max-width: var(--max-w-content); + color: var(--color-accent); +} +.report__content .section-txt { + max-width: var(--max-w-content); + margin-inline: auto; +} +.report__content .subsection-w-media { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--padding-body); + margin-bottom: calc(var(--spacing) * 4); + margin-top: calc(var(--spacing) * 2); + position: relative; +} +.report__content .subsection-w-media .media { + margin-inline: auto; + max-width: var(--max-w-content); + padding-left: var(--padding-inner); + padding-right: var(--padding-inner); + position: sticky; + top: calc(var(--header-h) + var(--spacing)); + align-self: start; +} +.report__content .subsection-txt { + min-height: calc(100vh - var(--header-h)); + max-width: var(--max-w-content); + padding-left: var(--padding-inner); + padding-right: var(--padding-inner); + padding-bottom: calc(var(--spacing) * 6); +} +.report__content .subsection-w-hscroll { + position: relative; + margin-bottom: calc(var(--spacing) * 4); +} +.report__content .subsection-w-hscroll .horizontal-scroll { + height: 100vh; + width: 100vw; + overflow: hidden; + display: flex; + align-items: center; +} +.report__content .subsection-w-hscroll .horizontal-scroll-wrapper { + display: flex; + flex-wrap: nowrap; + will-change: transform; +} +.report__content .subsection-w-hscroll .horizontal-scroll-slide { + flex-shrink: 0; + width: 90vw; + max-width: 700px; + padding: 0 calc(var(--spacing) * 1); +} +.report__content .subsection-w-hscroll .horizontal-scroll-slide figure, .report__content .subsection-w-hscroll .horizontal-scroll-slide img { + width: 100%; +} +.report__content .subsection-w-hscroll .horizontal-scroll-pagination { + position: fixed; + bottom: calc(var(--spacing) * 2); + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 8px; + z-index: 10; +} +.report__content p { + margin: calc(var(--spacing) * 0.75) 0; +} +.report__content ul { + padding-left: 3ch; +} +.report__content ul li { + margin: calc(var(--spacing) * 0.5) 0; +} + +.report__content:target { + padding-top: calc(var(--header-h) * 2 + var(--spacing)) !important; +} + +.media video { + width: 100%; +} +.media figure { + height: auto; +} +.media .caption { + font-size: var(--fs-small); + color: var(--color-txt-light); + font-weight: 500; + line-height: 1.1; +} +.media .swiper { + width: calc(100% - 60px); + max-width: 600px; +} + +#toggle-panel { + width: calc(var(--h-block) * 1); + padding: 0; +} + +#report__aside { + --border-aside: 1px solid var(--color-txt); +} +#report__aside .panel__header { + display: flex; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 0.5ch; + cursor: pointer; +} +#report__aside .panel__header .icon { + width: calc(var(--h-block) * 1.5); + display: flex; + align-items: center; + justify-content: center; +} +#report__aside .panel__header svg { + width: 16px; + fill: var(--color-txt); +} +#report__aside .panel__header .text { + padding-top: 5px; +} +#report__aside .panel__header .close { + position: absolute; + right: 0; +} +#report__aside .panel__header .close svg { + width: 10px; + fill: var(--color-txt); +} +#report__aside .panel__header:hover .close svg { + fill: var(--grey-200); +} +#report__aside .panel__content { + padding: var(--padding-body); + overflow: scroll; + scrollbar-width: none; + -ms-overflow-style: none; + border: var(--border-aside); + border-top: none; +} +#report__aside .panel__content::-webkit-scrollbar { + display: none; +} +#report__aside #toc ul { + list-style: none; +} +#report__aside #toc li { + font-size: var(--fs-small); + color: var(--color-txt-light); +} +#report__aside #toc li a { + display: block; + padding: 5px 1ch; + padding-top: 7px; +} +#report__aside #toc li.selected { + background-color: var(--grey-800); + color: var(--color-txt); +} +#report__aside #toc li:hover { + background-color: var(--grey-800); +} +#report__aside #toc .toc-level-1 { + margin-bottom: calc(var(--spacing) * 0.5); + font-weight: bold; +} +#report__aside #toc .toc-level-2 { + margin-bottom: calc(var(--spacing) * 0.25); + padding-left: 4ch; +} +#report__aside #toc a { + text-decoration: none; +} +#report__aside .btn--group { + margin-top: calc(var(--spacing) * 0.5); + display: flex; + gap: var(--padding-inner); +} +#report__aside .btn--group button { + flex-grow: 1; +} + +.content-package { + max-width: var(--max-w-cards); + margin: 0 auto; + display: grid; + grid-gap: calc(var(--padding-body) * 1.5); + position: relative; +} +.content-package .container-cards { + display: block; + align-self: start; +} +.content-package #section__investigations article { + margin-bottom: calc(var(--spacing) * 1); +} +.content-package .container__title { + font-weight: normal; + font-size: var(--fs-small); + font-weight: 500; + text-transform: uppercase; + margin-bottom: calc(var(--spacing) * 0.75); +} +@media screen and (max-width: 1080px) { + .content-package #section__investigations { + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: calc(var(--padding-inner) * 1.5) !important; margin-bottom: calc(var(--spacing) * 3); } - [data-template=investigation-summary] main #section__short { - margin-top: var(--padding-body); + .content-package #section__investigations article { + margin-bottom: 0px; } - [data-template=investigation-summary] main header { - max-width: var(--max-w-content); - margin: calc(var(--spacing) * 2) auto; + .content-package #section__investigations .container__title { + grid-column: span 2; + margin-bottom: 0px; } - [data-template=investigation-summary] main .section__article .section__title { - font-size: var(--fs-medium); - margin-bottom: var(--spacing); - } - [data-template=investigation-summary] main #section__synthese { - font-size: var(--fs-medium); - } - .modal--share { - position: absolute; - bottom: calc(var(--padding-body) + var(--h-block) + var(--spacing) * 0.25); - width: calc(100% - var(--padding-body)); + .content-package section:target { + padding-top: calc(var(--header-h) + var(--spacing)); } } @media screen and (min-width: 1080px) { - [data-template=investigation-summary] main #banner--page { - width: calc((100% - var(--max-w-content)) / 2); + .content-package { + grid-template-columns: 1fr 1fr; } - [data-template=investigation-summary] main #hero { - margin: calc(var(--spacing) * 3) 0; + .content-package #nav-package { + display: none; } - [data-template=investigation-summary] main #hero figcaption { - max-width: var(--max-w-content); - margin: 0 auto; + .content-package #section__investigations { + display: block; + margin-bottom: 0px; } - [data-template=investigation-summary] main header { - max-width: var(--max-w-content); - margin: calc(var(--spacing) * 2) auto; + .content-package #section__investigations article { + margin-bottom: calc(var(--spacing) * 1); } - [data-template=investigation-summary] main .section__article { - max-width: var(--max-w-content); - margin: calc(var(--spacing) * 3) auto; +} +@media screen and (max-width: 768px) { + .content-package { + display: block; + } + .content-package #section__investigations { + display: block; + } + .content-package .container__title { + margin-bottom: calc(var(--spacing) * 0.5) !important; + } +} + +#nav-package { + display: flex; + margin-top: calc(var(--spacing) * -1); + margin-bottom: calc(var(--spacing) * 2); +} +#nav-package svg { + width: 10px; + height: 10px; + transform: rotate(90deg); +} + +@media screen and (min-width: 1080px) { + #nav-package { + display: none; } }/*# sourceMappingURL=style.css.map */ \ No newline at end of file diff --git a/assets/css/style.css.map b/assets/css/style.css.map index 956c407..7cadba6 100644 --- a/assets/css/style.css.map +++ b/assets/css/style.css.map @@ -1 +1 @@ -{"version":3,"sources":["style.css","base/_var.scss","base/_body.scss","partials/_site-header.scss","components/_buttons.scss","components/_tags.scss","components/_keywords.scss","components/_form-newsletter.scss","components/_list-socials.scss","components/_modal-share.scss","components/_text.scss","components/_card-article.scss","components/_figures.scss","components/_card-article-small.scss","components/_card-impact.scss","components/_details-summary.scss","components/_card-open-graph.scss","partials/_site-menu.scss","partials/_site-footer.scss","partials/_main-layout.scss","template/_investigations.scss","template/_investigation-summary.scss"],"names":[],"mappings":"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","file":"style.css"} \ No newline at end of file +{"version":3,"sources":["style.css","base/_var.scss","base/_body.scss","partials/_site-header.scss","components/_buttons.scss","components/_tags.scss","components/_keywords.scss","components/_sort.scss","components/_btn-group-mobile.scss","components/_summary-hero.scss","components/_form-newsletter.scss","components/_search-form.scss","components/_list-socials.scss","components/_modal-share.scss","components/_dropdown.scss","components/_card-article.scss","components/_figures.scss","components/_card-article-small.scss","components/_card-impact.scss","components/_card-impact-small.scss","components/_details-summary.scss","components/_card-package.scss","components/_card-open-graph.scss","components/_swiper.scss","components/_slider-before-after.scss","partials/_site-menu.scss","partials/_site-footer.scss","partials/_main-layout.scss","partials/_page-header.scss","partials/_container-cards.scss","template/_home.scss","template/_investigation-summary.scss","template/_report.scss","template/_package.scss"],"names":[],"mappings":"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","file":"style.css"} \ No newline at end of file diff --git a/assets/css/style.scss b/assets/css/style.scss index d117e9f..9ae6a61 100644 --- a/assets/css/style.scss +++ b/assets/css/style.scss @@ -1,35 +1,45 @@ @charset "UTF-8"; -@import 'base/responsive'; -@import 'base/var'; -@import 'base/body'; +@import "base/responsive"; +@import "base/var"; +@import "base/body"; -@import 'partials/site-header'; +@import "partials/site-header"; // @import 'components/nav-tabs'; // @import 'components/btn--default'; -@import 'components/buttons'; -@import 'components/tags'; -@import 'components/keywords'; -@import 'components/details-summary'; +@import "components/buttons"; +@import "components/tags"; +@import "components/keywords"; +@import "components/details-summary"; +@import "components/sort"; +@import "components/btn-group-mobile"; -@import 'components/figures'; -@import 'components/form-newsletter'; -@import 'components/list-socials'; -@import 'components/modal-share'; -@import 'components/text'; -@import 'components/card-article'; -@import 'components/card-article-small'; -@import 'components/card-impact'; -@import 'components/card-open-graph'; - - -@import 'partials/site-header'; -@import 'partials/site-menu'; -@import 'partials/site-footer'; -@import 'partials/main-layout'; - - -@import 'template/investigations'; -@import 'template/investigation-summary'; +@import "components/figures"; +@import "components/summary-hero"; +@import "components/form-newsletter"; +@import "components/search-form"; +@import "components/list-socials"; +@import "components/modal-share"; +@import "components/dropdown"; +@import "components/text"; +@import "components/card-article"; +@import "components/card-article-small"; +@import "components/card-impact"; +@import "components/card-impact-small"; +@import "components/card-package"; +@import "components/card-open-graph"; +@import "components/swiper"; +@import "components/slider-before-after"; +@import "partials/site-header"; +@import "partials/site-menu"; +@import "partials/site-footer"; +@import "partials/main-layout"; +@import "partials/page-header"; +@import "partials/container-cards"; +@import "template/home"; +@import "template/investigation-summary"; +@import "template/report"; +@import "template/package"; +@import "template/impacts"; diff --git a/assets/css/template/_home.scss b/assets/css/template/_home.scss new file mode 100644 index 0000000..71dc2bc --- /dev/null +++ b/assets/css/template/_home.scss @@ -0,0 +1,162 @@ +.section--home { + + @media #{$small-up} { + border-bottom: var(--border-light); + } + + @media #{$medium} { + padding-bottom: calc(var(--spacing)*4); + margin-bottom: calc(var(--spacing)*4); + &:first-of-type{ + padding-top: calc(var(--spacing)*2); + } + .col-left{ + margin-bottom: calc(var(--spacing)*3); + } + } + + .baseline-section { + max-width: 42ch; + font-size: var(--fs-medium); + line-height: 1.1; + margin-bottom: calc(var(--spacing)*1); + } + + .btn--bold-inline{ + text-transform: none; + font-weight: 500; + font-size: var(--fs-small); + .icon{ + position: relative; + top: 2px; + } + svg{ + width: 13px; + height: 13px; + } + } + + + @media #{$medium-up} { + + .section--inner { + max-width: 1600px; + margin-inline: auto; + margin-bottom: calc(var(--spacing)*4); + padding-top: calc(var(--spacing)*4); + display: grid; + --gap: calc(var(--padding-body)*2); + grid-template-columns: 1fr 2fr; + grid-gap: var(--gap); + } + + + .col-left { + align-self: start; + position: sticky; + top: calc(var(--header-h) + var(--spacing)*4); + + } + + .col-right { + width: 100%; + height: 100%; + } + + + } + + + @media #{$small} { + + .card--article, + .card--impact{ + margin-bottom: calc(var(--spacing)*1); + } + + .baseline-section{ + font-size: var(--fs-big); + + } + + + } + + + + + + + + +} + + + + +#home__investigations { + + @media #{$small-up} { + .col-right{ + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--padding-body); + + .card--article:first-of-type{ + grid-column: span 2; + + display: grid; + grid-template-columns: 1fr 1fr; + + figure, + picture{ + grid-column: span 2; + } + + .title{ + grid-column: span 2; + font-size: var(--fs-medium); + padding-right: 3ch; + margin-bottom: calc(var(--spacing)*1); + } + + .description{ + grid-column: 1; + grid-row: 3; + padding-right: 3ch; + display: block; + -webkit-line-clamp: unset; + -webkit-box-orient: unset; + overflow: visible; + } + + dl{ + grid-column: 2; + grid-row: 3; + } + + .keywords-wrapper{ + grid-column: 2; + grid-row: 4; + } + } + + .see-more{ + grid-column: span 2; + // margin-top: var(--padding-body); + } + } + + } + + + +} + + + + + + + + diff --git a/site/cache/index.html b/assets/css/template/_impacts.scss similarity index 100% rename from site/cache/index.html rename to assets/css/template/_impacts.scss diff --git a/assets/css/template/_investigation-summary.scss b/assets/css/template/_investigation-summary.scss index fe66d59..68b07f2 100644 --- a/assets/css/template/_investigation-summary.scss +++ b/assets/css/template/_investigation-summary.scss @@ -1,362 +1,175 @@ -[data-template="investigation-summary"] main{ +[data-template="investigation-summary"] main { + position: relative; - position: relative; - + .page__header { + margin-inline: auto; + max-width: var(--max-w-content); + } + .panel-left { + width: calc((100vw - var(--max-w-cards) - var(--padding-body) * 4) * 0.5); + } - header{ - .page-type{ - text-transform: uppercase; - color: var(--color-txt-light); - margin-bottom: calc(var(--spacing)*0.5); - } - h2{ - font-size: var(--fs-xbig); - line-height: var(--leading-tight); - } + @media screen and (max-width: 1340px) { + margin-left: auto; + margin-right: calc(var(--padding-body) * 3); + .panel-left { + width: calc(100vw - var(--max-w-cards) - var(--padding-body) * 6); + } + } + + @media screen and (max-width: 1220px) { + margin-left: auto; + margin-right: 0px; + width: calc(100% - var(--panel-w) * 0.5 - var(--padding-body)); + .panel-left { + width: calc(var(--panel-w) * 0.5); + } + } + + .section__article { + margin-top: calc(var(--spacing) * 3); + margin-bottom: calc(var(--spacing) * 3); + max-width: var(--max-w-content); + margin-inline: auto; + + &:target { + padding-top: calc(var(--header-h) + var(--spacing) * 1); } - .section__article{ - a:hover{ - color: var(--grey-200); - } - .section__title{ - font-weight: normal; - text-transform: uppercase; - margin-bottom: calc(var(--spacing)*0.5) - } + a:hover { + color: var(--grey-200); } - #hero{ - width: 100vw; - position: relative; - left: calc(var(--padding-body)*-1); + .section__title { + font-weight: 500; + text-transform: uppercase; + margin-bottom: calc(var(--spacing) * 1); + padding-right: 2ch; + text-wrap: balance; + } + } - figcaption{ - color: var(--color-txt-light); - font-size: var(--fs-small); - @media #{$x-small}{ font-size: var(--fs-xsmall); } - padding-top: calc(var(--spacing)*0.5); - - - } + #section__dl, + #section__impacts, + #section__package, + #section__related-articles { + font-size: var(--fs-small); + } - &.hero-video{ - figure{ - width: 100%; + #section__dl { + margin-top: calc(var(--spacing) * 1.5); + border-bottom: var(--border-light); + max-width: var(--max-w-content); + margin-inline: auto; - img{ - width: 100%; - aspect-ratio: 16/9; - object-fit: cover; - } - - - } - } + .dl__group { + @include grid-content(); + column-gap: 1ch; + border-top: var(--border-light); + padding: calc(var(--spacing) * 0.5) 0; } - #nav--page{ - ul{ - list-style: none; - li{ - text-align: center; - color: var(--color-txt-light); - a{ - display: block; - padding: 0.3em 0; - text-decoration: none; - } - } - } + dt { + color: var(--color-txt-light); + padding-right: 1ch; } - #section__short{ - font-size: var(--fs-medium); + ul:not(.keywords) { + list-style: none; + + li { + padding-bottom: 0.2em; + } + } + } + + #section__synthese { + max-width: var(--max-w-content); + margin-inline: auto; + + p + p { + margin-top: 1em; } - #section__dl{ - margin-top: calc(var(--spacing)*1.5); - border-bottom: var(--border-light); + h4 { + margin-top: 2em; + margin-bottom: 1em; + font-size: var(--fs-normal); + font-weight: 500; + text-decoration: 1px underline var(--color-txt-light); + text-underline-offset: 3px; + } + } - .dl__group{ - @include grid-content(); - border-top: var(--border-light); - padding: calc(var(--spacing)*0.5) 0; - } + .panel-left { + //background-color: yellow; - dt{ - color: var(--color-txt-light); - padding-right: 1ch; - } + height: calc(100vh - var(--header-h)); + position: fixed; + left: var(--padding-body); + padding-bottom: calc(var(--padding-body) * 1); + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: center; + z-index: calc(var(--z-header) - 1); - ul:not(.keywords){ - list-style: none; - li{ - padding-bottom: 0.2em; - } - + #nav--page { + padding-bottom: var(--spacing); + width: calc(var(--panel-w) * 0.5); + ul { + list-style: none; + + li { + text-align: center; + color: var(--color-txt-light); + margin-bottom: 4px; + font-weight: 500; + font-size: var(--fs-small); + + a { + display: block; + padding: 5px 1ch; + text-decoration: none; + } } + } } + .btn--group { + width: calc(var(--panel-w) * 0.5); + display: flex; + flex-direction: column; + gap: calc(var(--spacing) * 0.25); - #section__synthese{ - p + p{ - margin-top: 0.5em; - } - - h4{ - margin-top: 2em; - margin-bottom: 1em; - font-size: var(--fs-normal); - font-weight: normal; - text-decoration: 1px underline var(--color-txt-light); - text-underline-offset: 3px; - - } - + button, + .dropdown { + flex-grow: 1; + width: 100%; + } } - - - + } } - - -// SHARE ACTIONS -------------------------------------------------- -// ---------------------------------------------------------------- - -#share-banner__desktop{ display: none; } -#share-banner__desktop ~ .modal--share{ - opacity: 0; - pointer-events: none; - transition: opacity .2s ease-in; -} - -#share-banner__desktop:checked ~ .modal--share{ - opacity: 1; - pointer-events: auto; -} - - - - - - // SMALL ---------------------------------------------------------- // ---------------------------------------------------------------- +@media #{$small} { + [data-template="investigation-summary"] main { + width: 100%; -@media #{$small}{ - - [data-template="investigation-summary"] main{ - - header{ - padding-top: calc(var(--spacing)*1.5); - .page-type{ - font-size: var(--fs-small); - } - } - - .section__article{ - margin: calc(var(--spacing)*1.5) 0; - } - - #section__impacts, #section__en-lien{ - margin-top: 0px; - } - - #hero{ - margin: calc(var(--spacing)*1.5) 0; - figcaption{ - margin: 0 var(--padding-body); - } - } - - .modal--share{ - position: absolute; - width: calc(100% - var(--padding-body)*2); - bottom: calc(var(--spacing) * 2); - } - - - #banner--page { - padding: calc(var(--spacing)*0.5) 0; - - position: fixed; - bottom: 0; - left: 0; - width: 100vw; - padding: calc(var(--spacing)*0.75) var(--padding-body); - padding-top: var(--spacing); - background-color: var(--color-bg); - background: linear-gradient(0deg,var(--color-bg) 0%, var(--color-bg) 64%, transparent 100%); - z-index: 800; - - #nav--page{ - display: none; - } - .btn--group{ - - display: flex; - gap: calc(var(--spacing)*0.25); - position: relative; - - > button, - > label { - width: 50%; - cursor: pointer; - } - - } - - } - - - // action - - #banner--page{ - opacity: 0; - pointer-events: none; - transition: opacity .2s ease-in; - } - - #banner--page.is-visible{ - opacity: 1; - pointer-events: auto; - } - - - + header { + margin-bottom: calc(var(--spacing) * 1); } + .section__article { + margin-top: calc(var(--spacing) * 2); + margin-bottom: calc(var(--spacing) * 2); + } + + .panel-left { + display: none; + } + } } - - - -@media #{$x-small}{ - [data-template="investigation-summary"] main{ - #section__dl .dl__group{ - - column-gap: 1ch; - font-size: var(--fs-small); - padding: calc(var(--spacing)*0.25) 0; - } - } -} - - - - - -// DESKTOP ---------------------------------------------------------- -// ---------------------------------------------------------------- - - -@media #{$small-up}{ - - [data-template="investigation-summary"] main{ - - #banner--page{ - height: calc(100vh - var(--header-h)); - height: calc(100dvh - var(--header-h)); - margin-bottom: calc((100vh - var(--header-h))*-1); - margin-bottom: calc((100dvh - var(--header-h))*-1); - padding: var(--padding-body); - padding-left: 0px; - position: sticky; - top: var(--header-h); - width: var(--banner-medium); - display: flex; - flex-direction: column; - justify-content: space-between; - } - - #banner--page .btn--group{ - display: flex; - flex-direction: column; - align-items: center; - gap: calc(var(--spacing)*0.25); - - > button, - > label { - width: 100%; - max-width: 160px; - cursor: pointer; - } - - } - - #hero{ - margin: calc(var(--spacing)*3) 0; - - } - - .section__article{ - margin-left: var(--banner-medium); - margin-top: calc(var(--spacing)*3); - margin-bottom: calc(var(--spacing)*3); - } - - #section__short{ - margin-top: var(--padding-body); - } - - header{ - max-width: var(--max-w-content); - margin: calc(var(--spacing)*2) auto; - } - - .section__article{ - .section__title{ - font-size: var(--fs-medium); - margin-bottom: var(--spacing); - } - } - - #section__synthese{ - font-size: var(--fs-medium); - } - } - .modal--share{ - position: absolute; - bottom: calc(var(--padding-body) + var(--h-block) + var(--spacing) * 0.25); - width: calc(100% - var(--padding-body)); - } -} - - - - -@media #{$medium-up}{ - - [data-template="investigation-summary"] main{ - - #banner--page{ - width: calc((100% - var(--max-w-content))/2); - } - - #hero{ - margin: calc(var(--spacing)*3) 0; - figcaption{ - max-width: var(--max-w-content); - margin: 0 auto; - } - } - - - header{ - max-width: var(--max-w-content); - margin: calc(var(--spacing)*2) auto; - } - - .section__article{ - max-width: var(--max-w-content); - margin: calc(var(--spacing)*3) auto; - } - - - - - } -} \ No newline at end of file diff --git a/assets/css/template/_investigations.scss b/assets/css/template/_investigations.scss deleted file mode 100644 index bb92653..0000000 --- a/assets/css/template/_investigations.scss +++ /dev/null @@ -1,17 +0,0 @@ - -[data-template="investigations"] main{ - - #container-cards{ - max-width: var(--max-w-container); - margin: 0 auto; - display: grid; - grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); - grid-auto-rows: minmax(100px, auto); - grid-gap: var(--padding-body); - margin-bottom: 10vh; - - - - } - -} diff --git a/assets/css/template/_package.scss b/assets/css/template/_package.scss new file mode 100644 index 0000000..263e37f --- /dev/null +++ b/assets/css/template/_package.scss @@ -0,0 +1,88 @@ +.content-package { + // max-width: 1300px; + max-width: var(--max-w-cards); + margin: 0 auto; + display: grid; + grid-gap: calc(var(--padding-body) * 1.5); + // grid-template-columns: 60% cacl(40% - var(--padding-body)*1.5); + + position: relative; + + .container-cards { + display: block; + align-self: start; + } + + #section__investigations article { + margin-bottom: calc(var(--spacing) * 1); + } + + .container__title { + font-weight: normal; + font-size: var(--fs-small); + font-weight: 500; + + text-transform: uppercase; + margin-bottom: calc(var(--spacing) * 0.75); + } + + @media #{$medium} { + #section__investigations { + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: calc(var(--padding-inner) * 1.5) !important; + margin-bottom: calc(var(--spacing) * 3); + article { + margin-bottom: 0px; + } + .container__title { + grid-column: span 2; + margin-bottom: 0px; + } + } + section:target { + padding-top: calc(var(--header-h) + var(--spacing)); + } + } + + @media #{$medium-up} { + #nav-package { + display: none; + } + grid-template-columns: 1fr 1fr; + #section__investigations { + display: block; + margin-bottom: 0px; + article { + margin-bottom: calc(var(--spacing) * 1); + } + } + } + + @media #{$small} { + display: block; + #section__investigations { + display: block; + } + .container__title { + margin-bottom: calc(var(--spacing) * 0.5) !important; + } + } +} + +#nav-package { + display: flex; + margin-top: calc(var(--spacing) * -1); + margin-bottom: calc(var(--spacing) * 2); + + svg { + width: 10px; + height: 10px; + transform: rotate(90deg); + } +} +@media #{$medium-up} { + #nav-package { + display: none; + } +} diff --git a/assets/css/template/_report.scss b/assets/css/template/_report.scss new file mode 100644 index 0000000..4d94e34 --- /dev/null +++ b/assets/css/template/_report.scss @@ -0,0 +1,518 @@ + + + + +// LAOUT ----------------------------------- + + +@media #{$small}{ + [data-template="report"] { + + #toggle-panel{ + display: none; + } + + #report__aside{ + width: 100vw; + position: fixed; + top: 0px; + height: 100vh; + left: -100vw; + z-index: calc(var(--z-header) + 10); + transition: left .4s ease-in; + background: var(--color-bg); + + + .panel__header{ + margin: 0 var(--padding-body); + border-bottom: var(--border-aside); + height: var(--header-h); + } + + .panel__content{ + height: calc(100dvh - var(--header-h)); + height: calc(100vh - var(--header-h)); + padding-bottom: 30vh; + } + + .btn--group{ + display: none; + } + + } + + .panel-open #report__aside{ + left: 0px; + } + + } +} + + + + +@media #{$small-up}{ +[data-template="report"] { + + + + + + + + #toggle-panel { + position: fixed; + top: var(--header-h); + left: var(--padding-body); + z-index: calc(var(--z-header) + 100); + } + + #report__aside { + position: fixed; + top: var(--header-h); + left: var(--padding-body); + width: var(--panel-w); + height: calc(100vh - var(--header-h)); + z-index: calc(var(--z-header) + 200); + background-color: var(--color-bg); + box-shadow: 4px 0px 4px 1px var(--color-bg); + + .panel__header{ + border: var(--border-aside); + height: calc(var(--h-block)*1.5); + } + + .panel__content { + height: calc(100% - var(--h-block)*4.25); + padding-bottom: 80px; + + } + + } + + + #report__aside { + left: calc(var(--panel-w)*-1); + transition: left ease-in-out .5s; + } + #report { + // padding-left: calc(var(--padding-body)*2); + transition: padding-left ease-in-out .5s; + } + + + .panel-open{ + + #report__aside{ + left: var(--padding-body); + } + #report { + padding-left: calc(var(--panel-w) + var(--padding-body)*1); + } + + } + + + + + +} + +} + +// HEADER ------------------------------------------------------------------------- + + + + + +.report__header { + + max-width: var(--max-w-cards); + margin-inline: auto; + margin-top: calc(var(--spacing) * 3.25); + margin-bottom: calc(var(--spacing) * 0.5); + + + display: grid; + column-gap: var(--padding-inner); + + grid-template-columns: 65% 35%; + grid-template-rows: auto auto 1fr; + position: relative; + + + padding: var(--padding-body); + + border: 1px solid var(--color-txt); + border-radius: var(--radius-small); + + .report__title-group { + grid-row: 1; + grid-column: span 2; + margin-bottom: calc(var(--spacing)*1); + + .title { + text-transform: uppercase; + font-weight: normal; + font-size: var(--fs-medium); + line-height: var(--leading-tight); + font-weight: 500; + margin-top: calc(var(--spacing)*1); + text-wrap: balance; + } + + .subtitle { + font-size: var(--fs-medium); + line-height: var(--leading-tight); + font-weight: 500; + text-wrap: balance; + + } + + } + + @include figure-16-9(); + + figure { + grid-row: 2; + grid-column: 2; + } + + .report__dl { + grid-row: 2; + grid-column: 1; + align-self: end; + font-size: var(--fs-small); + border-bottom: var(--border-light); + align-items: flex-start; + + .dl__group { + @include grid-content(); + border-top: var(--border-light); + padding: calc(var(--spacing)*0.5) 0; + } + + dt { + color: var(--color-txt-light); + padding-right: 1ch; + } + } + + + +} + + +.report__btns{ + + max-width: var(--max-w-cards); + margin-inline: auto; + + + display: flex; + justify-content: start; + flex-wrap: wrap; + align-items: start; + gap: calc(var(--spacing)*0.25); +} + + + + + +// CONTENT ------------------------------------------------------------------------------- + + + +.report__content { + + margin-top: calc(var(--spacing) * 4); + + .section-content { + // min-height: calc(100vh - var(--header-h)); + padding-left: calc(var(--padding-body)*1.5); + padding-right: calc(var(--padding-body)*1.5); + margin-bottom: calc(var(--spacing)*6); + &:target{ + padding-top: calc(var(--header-h) + var(--spacing)*2); + @media #{$small}{ padding-top: calc(var(--header-h) + var(--spacing)*0.5); } + } + + } + + + + .section-title { + + max-width: var(--max-w-content); + margin-inline: auto; + margin-bottom: calc(var(--spacing)*2); + + font-size: var(--fs-medium); + + font-weight: 500; + // text-transform: uppercase; + text-wrap: balance; + max-width: var(--max-w-content); + color: var(--color-accent); + } + + .section-txt{ + max-width: var(--max-w-content); + margin-inline: auto; + + + } + + .subsection-w-media{ + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--padding-body); + margin-bottom: calc(var(--spacing)*4); + margin-top: calc(var(--spacing)*2); + position: relative; + + + .media{ + margin-inline: auto; + max-width: var(--max-w-content); + padding-left: var(--padding-inner); + padding-right: var(--padding-inner); + position: sticky; + top: calc(var(--header-h) + var(--spacing)); + align-self: start; + } + + } + + + .subsection-txt { + min-height: calc(100vh - var(--header-h)); + max-width: var(--max-w-content); + padding-left: var(--padding-inner); + padding-right: var(--padding-inner); + padding-bottom: calc(var(--spacing)*6); + } + + .subsection-w-hscroll{ + position: relative; + margin-bottom: calc(var(--spacing)*4); + + .horizontal-scroll{ + height: 100vh; + width: 100vw; + overflow: hidden; + display: flex; + align-items: center; + } + + .horizontal-scroll-wrapper{ + display: flex; + flex-wrap: nowrap; + will-change: transform; + } + + .horizontal-scroll-slide{ + flex-shrink: 0; + width: 90vw; + max-width: 700px; + padding: 0 calc(var(--spacing) * 1); + + figure, img{ + width: 100%; + } + } + + .horizontal-scroll-pagination{ + position: fixed; + bottom: calc(var(--spacing) * 2); + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 8px; + z-index: 10; + } + + } + + + + p { + margin: calc(var(--spacing)*0.75) 0; + } + + ul { + padding-left: 3ch; + + li{ + margin: calc(var(--spacing)*0.5) 0; + } + } +} + +.report__content:target { + padding-top: calc(var(--header-h)*2 + var(--spacing)) !important; +} + + +.media{ + + video{ + width: 100%; + } + + figure{ + height: auto; + } + + .caption{ + font-size: var(--fs-small); + color: var(--color-txt-light); + font-weight: 500; + line-height: 1.1; + } + + .swiper{ + width: calc(100% - 60px); + max-width: 600px; + } +} + + + +/// PANEL-LEFT  -------------------------------------------- + + + +#toggle-panel { + width: calc(var(--h-block) * 1); + padding: 0; +} + +#report__aside { + --border-aside: 1px solid var(--color-txt); + + .panel__header { + display: flex; + + display: flex; + align-items: center; + justify-content: flex-start; + gap: 0.5ch; + + .icon { + width: calc(var(--h-block)*1.5); + display: flex; + align-items: center; + justify-content: center; + } + + svg { + width: 16px; + fill: var(--color-txt); + } + + .text { + padding-top: 5px; + } + + .close { + position: absolute; + right: 0; + + svg { + width: 10px; + fill: var(--color-txt); + } + + + } + + cursor: pointer; + + &:hover { + .close svg { + fill: var(--grey-200); + } + } + + + } + + .panel__content { + padding: var(--padding-body); + overflow: scroll; + @include hide-scroll(); + border: var(--border-aside); + border-top: none; + } + + + #toc { + ul { + list-style: none; + } + + li { + font-size: var(--fs-small); + + color: var(--color-txt-light); + + a { + display: block; + padding: 5px 1ch; + padding-top: 7px; + } + + + } + + li.selected { + background-color: var(--grey-800); + color: var(--color-txt); + } + + li:hover { + background-color: var(--grey-800); + } + + .toc-level-1 { + margin-bottom: calc(var(--spacing)*0.5); + font-weight: bold; + } + + .toc-level-2 { + margin-bottom: calc(var(--spacing)*0.25); + padding-left: 4ch; + } + + a { + text-decoration: none; + } + + } + + .btn--group { + margin-top: calc(var(--spacing)*0.5); + display: flex; + gap: var(--padding-inner); + + button { + flex-grow: 1; + } + } + + + + +} + + + + + + + + + diff --git a/assets/fonts/stylesheet.css b/assets/fonts/stylesheet.css index 95bf8b1..19d6fa5 100644 --- a/assets/fonts/stylesheet.css +++ b/assets/fonts/stylesheet.css @@ -2,22 +2,22 @@ @font-face { font-family: "Executive"; src: url("Executive-55Regular.woff") format("woff"); - font-weight: 300; + font-weight: normal; font-style: normal; } @font-face { font-family: "Executive"; src: url("Executive-56Italic.woff") format("woff"); - font-weight: 300; + font-weight: normal; font-style: italic; } @font-face { font-family: "Executive"; src: url("Executive-65Medium.woff") format("woff"); - font-weight: normal; - font-style: normal; + font-weight: 500; + font-style: 500; } @font-face { diff --git a/assets/icons/article.svg b/assets/icons/article.svg new file mode 100644 index 0000000..3c4d502 --- /dev/null +++ b/assets/icons/article.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/close.svg b/assets/icons/close.svg index d02ecbb..8a44ae2 100644 --- a/assets/icons/close.svg +++ b/assets/icons/close.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/filter.svg b/assets/icons/filter.svg new file mode 100644 index 0000000..30b3be1 --- /dev/null +++ b/assets/icons/filter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/folder.svg b/assets/icons/folder.svg new file mode 100644 index 0000000..562ed7b --- /dev/null +++ b/assets/icons/folder.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/pdf.svg b/assets/icons/pdf.svg new file mode 100644 index 0000000..8db44b1 --- /dev/null +++ b/assets/icons/pdf.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/pin.svg b/assets/icons/pin.svg new file mode 100644 index 0000000..ae58e20 --- /dev/null +++ b/assets/icons/pin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/play.svg b/assets/icons/play.svg new file mode 100644 index 0000000..4aee878 --- /dev/null +++ b/assets/icons/play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/printer.svg b/assets/icons/printer.svg new file mode 100644 index 0000000..141b66c --- /dev/null +++ b/assets/icons/printer.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/share.svg b/assets/icons/share.svg new file mode 100644 index 0000000..f61e992 --- /dev/null +++ b/assets/icons/share.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/icons/socials/bluesky.svg b/assets/icons/socials/bluesky.svg index 3944e93..a070aea 100644 --- a/assets/icons/socials/bluesky.svg +++ b/assets/icons/socials/bluesky.svg @@ -1,9 +1,3 @@ - - - - - - - - - + + + \ No newline at end of file diff --git a/assets/icons/socials/facebook.svg b/assets/icons/socials/facebook.svg index cf4118e..5fc7cec 100644 --- a/assets/icons/socials/facebook.svg +++ b/assets/icons/socials/facebook.svg @@ -1,3 +1,3 @@ - - - + + + \ No newline at end of file diff --git a/assets/icons/socials/instagram.svg b/assets/icons/socials/instagram.svg index 57775b0..855e653 100644 --- a/assets/icons/socials/instagram.svg +++ b/assets/icons/socials/instagram.svg @@ -1,3 +1,3 @@ - - - + + + \ No newline at end of file diff --git a/assets/icons/socials/linkedin.svg b/assets/icons/socials/linkedin.svg index 5a9c0a5..30fc0e3 100644 --- a/assets/icons/socials/linkedin.svg +++ b/assets/icons/socials/linkedin.svg @@ -1,3 +1,3 @@ - - - + + + \ No newline at end of file diff --git a/assets/icons/socials/mastodon.svg b/assets/icons/socials/mastodon.svg index efe42f5..a8c2a26 100644 --- a/assets/icons/socials/mastodon.svg +++ b/assets/icons/socials/mastodon.svg @@ -1,9 +1,3 @@ - - - - - - - - - + + + \ No newline at end of file diff --git a/assets/icons/socials/threads.svg b/assets/icons/socials/threads.svg index 3619982..13c9e7a 100644 --- a/assets/icons/socials/threads.svg +++ b/assets/icons/socials/threads.svg @@ -1,3 +1,3 @@ - - - + + + \ No newline at end of file diff --git a/assets/icons/socials/x-social.svg b/assets/icons/socials/x-social.svg index 18f2f4d..2fafcc2 100644 --- a/assets/icons/socials/x-social.svg +++ b/assets/icons/socials/x-social.svg @@ -1,9 +1,3 @@ - - - - - - - - - + + + \ No newline at end of file diff --git a/assets/icons/socials/youtube.svg b/assets/icons/socials/youtube.svg index 40ca348..3c9c0be 100644 --- a/assets/icons/socials/youtube.svg +++ b/assets/icons/socials/youtube.svg @@ -1,3 +1,3 @@ - - - + + + \ No newline at end of file diff --git a/assets/icons/toc.svg b/assets/icons/toc.svg new file mode 100644 index 0000000..ab417a6 --- /dev/null +++ b/assets/icons/toc.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/assets/images/SequenceOrbit_Finale_1245.png b/assets/images/SequenceOrbit_Finale_1245.png new file mode 100644 index 0000000..97365a2 Binary files /dev/null and b/assets/images/SequenceOrbit_Finale_1245.png differ diff --git a/assets/images/adam-raihane/fig-1-a.png b/assets/images/adam-raihane/fig-1-a.png new file mode 100644 index 0000000..d09aac1 Binary files /dev/null and b/assets/images/adam-raihane/fig-1-a.png differ diff --git a/assets/images/adam-raihane/fig-1-b.png b/assets/images/adam-raihane/fig-1-b.png new file mode 100644 index 0000000..44b2a10 Binary files /dev/null and b/assets/images/adam-raihane/fig-1-b.png differ diff --git a/assets/images/adam-raihane/fig-1.png b/assets/images/adam-raihane/fig-1.png new file mode 100644 index 0000000..8c1313f Binary files /dev/null and b/assets/images/adam-raihane/fig-1.png differ diff --git a/assets/images/adam-raihane/fig-2.png b/assets/images/adam-raihane/fig-2.png new file mode 100644 index 0000000..2e60668 Binary files /dev/null and b/assets/images/adam-raihane/fig-2.png differ diff --git a/assets/images/adam-raihane/fig-3.png b/assets/images/adam-raihane/fig-3.png new file mode 100644 index 0000000..e203433 Binary files /dev/null and b/assets/images/adam-raihane/fig-3.png differ diff --git a/assets/images/adam-raihane/fig-4.png b/assets/images/adam-raihane/fig-4.png new file mode 100644 index 0000000..4777605 Binary files /dev/null and b/assets/images/adam-raihane/fig-4.png differ diff --git a/assets/images/adam-raihane/fig-5.png b/assets/images/adam-raihane/fig-5.png new file mode 100644 index 0000000..618c108 Binary files /dev/null and b/assets/images/adam-raihane/fig-5.png differ diff --git a/assets/images/adam-raihane/fig-6.png b/assets/images/adam-raihane/fig-6.png new file mode 100644 index 0000000..3a6e0cc Binary files /dev/null and b/assets/images/adam-raihane/fig-6.png differ diff --git a/assets/images/adam-raihane/fig-7.png b/assets/images/adam-raihane/fig-7.png new file mode 100644 index 0000000..61e03f4 Binary files /dev/null and b/assets/images/adam-raihane/fig-7.png differ diff --git a/assets/images/adam-raihane/modele-3d-vehicule.mp4 b/assets/images/adam-raihane/modele-3d-vehicule.mp4 new file mode 100644 index 0000000..4c91b1d Binary files /dev/null and b/assets/images/adam-raihane/modele-3d-vehicule.mp4 differ diff --git a/assets/images/cover-rapport.png b/assets/images/cover-rapport.png new file mode 100644 index 0000000..276d94a Binary files /dev/null and b/assets/images/cover-rapport.png differ diff --git a/assets/images/eclairages.png b/assets/images/eclairages.png new file mode 100644 index 0000000..4a56639 Binary files /dev/null and b/assets/images/eclairages.png differ diff --git a/assets/images/image-rapport.png b/assets/images/image-rapport.png new file mode 100644 index 0000000..e129ed6 Binary files /dev/null and b/assets/images/image-rapport.png differ diff --git a/assets/js/banner-sticky-desktop.js b/assets/js/banner-sticky-desktop.js index 6f9e171..3495b43 100644 --- a/assets/js/banner-sticky-desktop.js +++ b/assets/js/banner-sticky-desktop.js @@ -4,19 +4,14 @@ export function bannerStickyDesktop(responsiveSmall) { if (isInitialized) return; let body = document.body; - if (!body || body.dataset.template !== 'investigation-summary') return; + let panel = body.querySelector(".panel-left"); + + if (!panel) return; - let bannerPage = body.querySelector("#banner--page"); - let sectionDl = document.querySelector("#section__dl"); let footer = document.querySelector("#site-footer"); - if (!bannerPage || !sectionDl || !footer) return; - // Stocker la hauteur initiale du banner - const bannerInitialHeight = bannerPage.offsetHeight; - - // Stocker la position initiale du footer (calculée une seule fois au chargement) - let footerInitialTop = footer.offsetTop; + const bannerInitialHeight = panel.offsetHeight; function checkScroll() { const screenWidth = window.innerWidth; @@ -24,33 +19,35 @@ export function bannerStickyDesktop(responsiveSmall) { // Vérifier que l'écran est plus grand que responsiveSmall if (screenWidth <= responsiveSmall) { // Réinitialiser le transform si on est en dessous de responsiveSmall - bannerPage.style.transform = ''; + panel.style.transform = ''; return; } // Calculer la position du bas de la fenêtre const windowBottom = window.scrollY + window.innerHeight; - // Calculer de combien on dépasse le haut du footer (position initiale) - const overlap = windowBottom - footerInitialTop; + // Calculer dynamiquement la position du footer à chaque scroll + // Utiliser getBoundingClientRect() + scrollY pour une valeur toujours à jour + const footerTop = footer.getBoundingClientRect().top + window.scrollY; + + // Calculer de combien on dépasse le haut du footer + const overlap = windowBottom - footerTop; if (overlap > 0) { // Le bas de la fenêtre a atteint le haut du footer // Déplacer le banner vers le haut du nombre de pixels de dépassement const translateValue = Math.min(overlap, bannerInitialHeight); - bannerPage.style.transform = `translateY(-${translateValue}px)`; + panel.style.transform = `translateY(-${translateValue}px)`; } else { // Réinitialiser la position si on n'a pas encore atteint le footer - bannerPage.style.transform = 'translateY(0)'; + panel.style.transform = 'translateY(0)'; } } window.addEventListener('scroll', checkScroll); window.addEventListener('resize', () => { - // Recalculer la position du footer lors du resize if (window.innerWidth > responsiveSmall) { - bannerPage.style.transform = ''; - footerInitialTop = footer.offsetTop; + panel.style.transform = ''; } checkScroll(); }); diff --git a/assets/js/banner-sticky-mobile.js b/assets/js/banner-sticky-mobile.js deleted file mode 100644 index a0f512a..0000000 --- a/assets/js/banner-sticky-mobile.js +++ /dev/null @@ -1,50 +0,0 @@ -let isInitialized = false; - -export function bannerStickyMobile(responsiveSmall) { - if (isInitialized) return; - - let body = document.body; - if (!body || body.dataset.template !== 'investigation-summary') return; - - let bannerPage = body.querySelector("#banner--page"); - let sectionDl = document.querySelector("#section__dl"); - let footer = document.querySelector("#site-footer"); - - if (!bannerPage || !sectionDl || !footer) return; - - function checkScroll() { - const screenWidth = window.innerWidth; - - // Vérifier que l'écran est plus petit que responsiveSmall - if (screenWidth >= responsiveSmall) { - bannerPage.classList.remove('is-visible'); - bannerPage.style.transform = 'translateY(0)'; - return; - } - - const sectionTop = sectionDl.getBoundingClientRect().top; - const footerTop = footer.getBoundingClientRect().top; - const windowHeight = window.innerHeight; - - // Activer le banner quand #section__dl arrive en bas de l'écran - if (sectionTop <= windowHeight) { - bannerPage.classList.add('is-visible'); - - // Pousser le banner vers le haut si le footer arrive en bas de l'écran - if (footerTop < windowHeight) { - const pushUp = windowHeight - footerTop; - bannerPage.style.transform = `translateY(-${pushUp}px)`; - } else { - bannerPage.style.transform = 'translateY(0)'; - } - } else { - bannerPage.classList.remove('is-visible'); - bannerPage.style.transform = 'translateY(0)'; - } - } - - window.addEventListener('scroll', checkScroll); - checkScroll(); - - isInitialized = true; -} \ No newline at end of file diff --git a/assets/js/btn-group-mobile.js b/assets/js/btn-group-mobile.js new file mode 100644 index 0000000..518b6aa --- /dev/null +++ b/assets/js/btn-group-mobile.js @@ -0,0 +1,35 @@ +let isInitialized = false; + +export function btnGroupMobile() { + if (isInitialized) return; + const btnGroup = document.querySelector(".btn--group__mobile"); + let footer = document.querySelector("#site-footer"); + + if (!btnGroup) return; + + function checkScroll() { + + const windowHeight = window.innerHeight; + const scrollY = window.scrollY; + const footerTop = footer.getBoundingClientRect().top; + + if (scrollY > windowHeight * 0.6) { + btnGroup.classList.add('is-visible'); + + if (footerTop < windowHeight) { + btnGroup.classList.remove('is-visible'); + } + } else { + btnGroup.classList.remove('is-visible'); + } + } + + window.addEventListener('scroll', checkScroll); + checkScroll(); + + isInitialized = true; + + +} + + diff --git a/assets/js/dropdown.js b/assets/js/dropdown.js new file mode 100644 index 0000000..d254a9d --- /dev/null +++ b/assets/js/dropdown.js @@ -0,0 +1,87 @@ +export function initDropdowns(responsiveSmall) { + const dropdowns = document.querySelectorAll('.dropdown'); + + function updateBodyOverflow(isOpen, dropdownElement = null) { + const isInMobileGroup = dropdownElement?.closest('.btn--group__mobile'); + if (isOpen && window.innerWidth < responsiveSmall && isInMobileGroup) { + document.body.classList.add('is-hidden'); + } else { + document.body.classList.remove('is-hidden'); + } + } + + dropdowns.forEach(dropdown => { + const trigger = dropdown.querySelector('.dropdown__trigger'); + const content = dropdown.querySelector('.dropdown__content'); + + if (!trigger) return; + + + + // Empêche la fermeture au clic dans le contenu des dropdowns contenant .modal--share + if (dropdown.querySelector('.modal--share') && content) { + content.addEventListener('click', (e) => { + e.stopPropagation(); + }); + } + + trigger.addEventListener('click', (e) => { + e.stopPropagation(); + + // Ferme les autres dropdowns ouverts + dropdowns.forEach(other => { + if (other !== dropdown) { + other.classList.remove('is-open', 'dropdown--align-right'); + other.querySelector('.dropdown__trigger')?.classList.remove('is-selected'); + } + }); + + const isOpening = !dropdown.classList.contains('is-open'); + + if (isOpening) { + // Vérifie s'il y a la place à droite + const content = dropdown.querySelector('.dropdown__content'); + const triggerRect = trigger.getBoundingClientRect(); + const contentWidth = content.offsetWidth || 300; + const spaceRight = window.innerWidth - triggerRect.left; + + if (spaceRight < contentWidth) { + dropdown.classList.add('dropdown--align-right'); + } else { + dropdown.classList.remove('dropdown--align-right'); + } + } + + // Toggle le dropdown actuel + dropdown.classList.toggle('is-open'); + trigger.classList.toggle('is-selected'); + + // Gère l'overflow du body sur mobile + updateBodyOverflow(dropdown.classList.contains('is-open'), dropdown); + }); + }); + + // Ferme tous les dropdowns au clic extérieur + document.addEventListener('click', (e) => { + dropdowns.forEach(dropdown => { + // Ne ferme pas si le clic est dans un dropdown contenant .modal--share + if (dropdown.querySelector('.modal--share') && dropdown.contains(e.target)) { + return; + } + dropdown.classList.remove('is-open', 'dropdown--align-right'); + dropdown.querySelector('.dropdown__trigger')?.classList.remove('is-selected'); + }); + updateBodyOverflow(false); + }); + + // Ferme au press Escape + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + dropdowns.forEach(dropdown => { + dropdown.classList.remove('is-open', 'dropdown--align-right'); + dropdown.querySelector('.dropdown__trigger')?.classList.remove('is-selected'); + }); + updateBodyOverflow(false); + } + }); +} diff --git a/assets/js/header.js b/assets/js/header.js index 8c1e30a..0549ae3 100644 --- a/assets/js/header.js +++ b/assets/js/header.js @@ -1,26 +1,22 @@ export function headerToggle() { const header = document.getElementById("site-header"); const buttonToggle = document.querySelector("#menu-toggle"); - console.log(header); - console.log(buttonToggle); if (!header || !buttonToggle) return; buttonToggle.addEventListener("click", () => { - document.body.classList.toggle("menu-open"); + const isOpen = document.body.classList.toggle("menu-open"); }); } + +// DELETE ? export function headerScrollVisibility() { const header = document.getElementById("site-header"); - const hero = document.getElementById("hero"); - if (!header || !hero) return; + if (!header) return; function checkScroll() { - const headerHeight = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--header-h')) || 0; - const heroBottom = hero.getBoundingClientRect().bottom; - - if (heroBottom <= headerHeight) { + if (window.scrollY >= 300) { header.classList.add("is-visible"); } else { header.classList.remove("is-visible"); @@ -28,6 +24,6 @@ export function headerScrollVisibility() { } window.addEventListener("scroll", checkScroll); - checkScroll(); // Vérifier au chargement + checkScroll(); } diff --git a/assets/js/hero-slider.js b/assets/js/hero-slider.js new file mode 100644 index 0000000..dd39df8 --- /dev/null +++ b/assets/js/hero-slider.js @@ -0,0 +1,50 @@ +import Swiper from 'https://cdn.jsdelivr.net/npm/swiper@12/swiper-bundle.min.mjs'; + +export function initHeroSlider() { + const heroSlider = document.querySelector('.hero-slider'); + + if (!heroSlider) { + return; + } + + const swiper = new Swiper('.hero-slider', { + // Optional parameters + loop: true, + speed: 600, + effect: 'fade', + fadeEffect: { + crossFade: true + }, + + // Touch/Swipe settings (activé par défaut, mais configuré ici pour optimisation) + touchRatio: 1, + touchAngle: 45, + grabCursor: true, + simulateTouch: true, + allowTouchMove: true, + + // Navigation arrows + navigation: { + nextEl: '.swiper-button-next', + prevEl: '.swiper-button-prev', + }, + + // Pagination + pagination: { + el: '.swiper-pagination', + clickable: true, + }, + + // Keyboard control + keyboard: { + enabled: true, + }, + + // Accessibility + a11y: { + prevSlideMessage: 'Diapositive précédente', + nextSlideMessage: 'Diapositive suivante', + paginationBulletMessage: 'Aller à la diapositive {{index}}', + }, + }); +} diff --git a/assets/js/hero-video.js b/assets/js/hero-video.js new file mode 100644 index 0000000..707c316 --- /dev/null +++ b/assets/js/hero-video.js @@ -0,0 +1,39 @@ + +export function playVideo() { + const playButton = document.querySelector('#hero-play-video'); + + if (!playButton) { + return; + } + + playButton.addEventListener('click', function() { + const extract = document.querySelector('.extract'); + const videoFull = document.querySelector('.video-full'); + const titleSmall = document.querySelector('.page-title-small'); + + + if (extract) { + extract.style.display = 'none'; + } + + if(titleSmall){ + titleSmall.style.display = 'none'; + } + + if (videoFull) { + videoFull.style.display = 'block'; + + const iframe = videoFull.querySelector('iframe'); + if (iframe) { + const src = iframe.src; + + // Ajouter les paramètres autoplay et mute pour YouTube + if (src) { + const separator = src.includes('?') ? '&' : '?'; + iframe.src = src + separator + 'autoplay=1&mute=1'; + iframe.setAttribute('allow', 'autoplay; encrypted-media'); + } + } + } + }); +} diff --git a/assets/js/panel.js b/assets/js/panel.js new file mode 100644 index 0000000..7575010 --- /dev/null +++ b/assets/js/panel.js @@ -0,0 +1,60 @@ +export function panelToggle(responsiveSmall) { + const toggleBtn = document.querySelector('#toggle-panel'); + const toggleBtnMobile = document.querySelector('#toggle-panel__mobile'); + const main = document.querySelector('main'); + const closeBtn = document.querySelector('.panel-left .panel__header'); + + function openPanel() { + main.classList.add('panel-open'); + const screenWidth = window.innerWidth; + if (screenWidth <= responsiveSmall) { + console.log("small screen"); + document.body.style.overflowY = 'hidden'; + } + + } + + function closePanel() { + main.classList.remove('panel-open'); + main.classList.add('panel-close'); + document.body.style.overflowY = ''; + } + + if (toggleBtn) { + toggleBtn.addEventListener('click', (e) => { + e.stopPropagation(); + openPanel(); + }); + } + + if (toggleBtnMobile) { + toggleBtnMobile.addEventListener('click', (e) => { + e.stopPropagation(); + openPanel(); + }); + } + + if (closeBtn) { + closeBtn.addEventListener('click', closePanel); + } +} + + +export function tocMobile(responsiveSmall) { + const toc = document.querySelector('#toc'); + const main = document.querySelector('main'); + + if (!toc) return; + + const tocLinks = toc.querySelectorAll('a'); + + tocLinks.forEach(link => { + link.addEventListener('click', () => { + if (window.innerWidth <= responsiveSmall) { + main.classList.remove('panel-open'); + main.classList.add('panel-close'); + document.body.style.overflowY = ''; + } + }); + }); +} \ No newline at end of file diff --git a/assets/js/report.js b/assets/js/report.js new file mode 100644 index 0000000..c7f60d1 --- /dev/null +++ b/assets/js/report.js @@ -0,0 +1,135 @@ + +import { initSwipers } from './swipers.js'; + +export function report(responsiveSmall) { + if (document.body.dataset.template === 'report') { + + // Initialiser tous les sliders de type before-after + initSliderBeforeAfter(); + + initHorizontalScroll(); + + // Ne fonctionne que pour les écrans plus grands que responsiveSmall + if (window.matchMedia(responsiveSmall).matches) { + // Sur mobile : initialiser les swipers normalement car initMediaDisplay ne sera pas actif + initSwipers(); + return; + } + + + } +} + + +function initSliderBeforeAfter(container = document){ + const slidersBeforeAfter = container.querySelectorAll('.slider-before-after'); + slidersBeforeAfter.forEach(function (sliderContainer, index) { + const sliderInput = sliderContainer.querySelector('.slider'); + if (sliderInput) { + sliderInput.addEventListener('input', (e) => { + console.log('slider value:', e.target.value); + sliderContainer.style.setProperty('--position', `${e.target.value}%`); + }); + } + }); +} + + +function initHorizontalScroll(){ + const sections = document.querySelectorAll('.subsection-w-hscroll'); + + sections.forEach(function (section) { + const container = section.querySelector('.horizontal-scroll'); + if (!container) return; + + const wrapper = container.querySelector('.horizontal-scroll-wrapper'); + if (!wrapper) return; + + const slides = wrapper.querySelectorAll('.horizontal-scroll-slide'); + + // Calculer la distance totale à scroller horizontalement + function calculateScrollDistance() { + const totalSlidesWidth = Array.from(slides).reduce((acc, slide) => acc + slide.offsetWidth, 0); + const endMargin = window.innerWidth * 0.3; // 30vw de marge à la fin + return totalSlidesWidth - window.innerWidth + endMargin; + } + + let scrollDistance = calculateScrollDistance(); + + // Créer un spacer invisible qui crée l'espace de scroll + // Hauteur = scrollDistance + hauteur du viewport pour maintenir le texte en dessous + const spacer = document.createElement('div'); + spacer.className = 'horizontal-scroll-spacer'; + spacer.style.height = `${scrollDistance + window.innerHeight}px`; + spacer.style.width = '100%'; + spacer.style.pointerEvents = 'none'; + + // Insérer le spacer AVANT .horizontal-scroll + section.insertBefore(spacer, container); + + // Calculer la position absolue du spacer une seule fois + function getSpacerTopPosition() { + let element = spacer; + let top = 0; + while (element) { + top += element.offsetTop; + element = element.offsetParent; + } + return top; + } + + let spacerTopPosition = getSpacerTopPosition(); + + // Fonction de mise à jour du scroll horizontal + function updateHorizontalScroll() { + const scrollY = window.pageYOffset || document.documentElement.scrollTop; + + // Début et fin du scroll basé sur le spacer + const scrollStart = spacerTopPosition; + const scrollEnd = spacerTopPosition + scrollDistance; + + console.log('scrollY:', scrollY, 'scrollStart:', scrollStart, 'scrollEnd:', scrollEnd); + + if (scrollY >= scrollStart && scrollY <= scrollEnd) { + // Phase de scroll horizontal : fixer le container + const progress = (scrollY - scrollStart) / scrollDistance; + const translateX = progress * scrollDistance; + + console.log('Horizontal scroll active - progress:', progress); + + wrapper.style.transform = `translateX(-${translateX}px)`; + container.style.position = 'fixed'; + container.style.top = '0'; + container.style.left = '0'; + } else if (scrollY < scrollStart) { + // Avant le spacer : reset + console.log('Before spacer'); + wrapper.style.transform = 'translateX(0)'; + container.style.position = ''; + container.style.top = ''; + container.style.left = ''; + } else { + // Après le spacer : garder le translate final et défixer + console.log('After spacer'); + wrapper.style.transform = `translateX(-${scrollDistance}px)`; + container.style.position = ''; + container.style.top = ''; + container.style.left = ''; + } + } + + // Écouter le scroll + window.addEventListener('scroll', updateHorizontalScroll, { passive: true }); + + // Recalculer au resize + window.addEventListener('resize', function() { + scrollDistance = calculateScrollDistance(); + spacer.style.height = `${scrollDistance + window.innerHeight}px`; + spacerTopPosition = getSpacerTopPosition(); + updateHorizontalScroll(); + }); + + // Initial call + updateHorizontalScroll(); + }); +} \ No newline at end of file diff --git a/assets/js/script.js b/assets/js/script.js index 773fc50..9a405ed 100644 --- a/assets/js/script.js +++ b/assets/js/script.js @@ -1,18 +1,63 @@ import { headerToggle, headerScrollVisibility } from './header.js'; import { copyLink } from './share.js'; -import { bannerStickyMobile } from './banner-sticky-mobile.js'; +import { panelToggle, tocMobile } from './panel.js'; +import { btnGroupMobile } from './btn-group-mobile.js'; import { bannerStickyDesktop } from './banner-sticky-desktop.js'; import { themeToggle } from './themeToggle.js'; +import { initHeroSlider } from './hero-slider.js'; +import { playVideo } from './hero-video.js'; +import { initDropdowns } from './dropdown.js'; +import { initSwipers } from './swipers.js'; +import { report } from './report.js'; + const responsiveMedium = 1080; const responsiveSmall = 768; -window.onload = async function () { +window.onload = async function () { console.log("SCRIPT LOADED"); headerToggle(); - headerScrollVisibility(); - copyLink(); + panelToggle(responsiveSmall); themeToggle(); - bannerStickyMobile(responsiveSmall); + + + report(responsiveSmall); + + tocMobile(responsiveSmall); + copyLink(); + btnGroupMobile(responsiveSmall) bannerStickyDesktop(responsiveSmall); + initHeroSlider(); + playVideo(); + initDropdowns(responsiveSmall); + initSwipers(); + + + var elem = document.querySelector('.grid-masonry'); + var msnry = null; + + function initMasonry() { + if (!elem) return; + if (window.innerWidth >= responsiveSmall) { + if (!msnry) { + msnry = new Masonry(elem, { + itemSelector: '.card--impact', + columnWidth: '.grid-sizer', + percentPosition: true, + gutter: 26 + }); + } + } else { + if (msnry) { + msnry.destroy(); + msnry = null; + } + } + } + + initMasonry(); + window.addEventListener('resize', initMasonry); + + + } \ No newline at end of file diff --git a/assets/js/share.js b/assets/js/share.js index 5c42a65..c26fb8f 100644 --- a/assets/js/share.js +++ b/assets/js/share.js @@ -1,14 +1,16 @@ export function copyLink() { let buttons = document.querySelectorAll('.copy-link button'); buttons.forEach(function (button, index) { - let link = button.parentNode.querySelector("input").value; + let input = button.parentNode.querySelector("input"); + let link = input.value; button.addEventListener('click', function() { navigator.clipboard.writeText(link).then(() => { - const originalText = button.textContent; - button.textContent = 'Lien copié'; + input.value = 'Lien copié !'; + input.classList.add('is-copied'); setTimeout(() => { - button.textContent = originalText; + input.value = link; + input.classList.remove('is-copied'); }, 1000); }).catch(err => { console.error('Erreur lors de la copie:', err); diff --git a/assets/js/swipers.js b/assets/js/swipers.js new file mode 100644 index 0000000..c69ed3d --- /dev/null +++ b/assets/js/swipers.js @@ -0,0 +1,54 @@ +import Swiper from 'https://cdn.jsdelivr.net/npm/swiper@12/swiper-bundle.min.mjs'; + +export function initSwipers(container = document) { + const sliders = container.querySelectorAll('.swiper'); + + if (sliders.length === 0) { + return; + } + + sliders.forEach((sliderElement) => { + // Éviter de réinitialiser un swiper déjà initialisé + if (sliderElement.swiper) { + return; + } + + const swiper = new Swiper(sliderElement, { + // Optional parameters + slidesPerView: 1, + spaceBetween: 20, + speed: 600, + + // Touch/Swipe settings + touchRatio: 1, + touchAngle: 45, + grabCursor: true, + simulateTouch: true, + allowTouchMove: true, + + // Navigation arrows + navigation: { + nextEl: sliderElement.querySelector('.swiper-button-next'), + prevEl: sliderElement.querySelector('.swiper-button-prev'), + }, + + // Pagination + pagination: { + el: sliderElement.querySelector('.swiper-pagination'), + clickable: true, + }, + + // Keyboard control + keyboard: { + enabled: true, + }, + + // Accessibility + a11y: { + prevSlideMessage: 'Investigation précédente', + nextSlideMessage: 'Investigation suivante', + paginationBulletMessage: 'Aller à l\'investigation {{index}}', + }, + }); + }); +} diff --git a/composer.json b/composer.json index d3864ae..b5b2a81 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,8 @@ }, "require": { "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", - "getkirby/cms": "^5.0" + "getkirby/cms": "^5.0", + "tobimori/kirby-seo": "^2.0.0-alpha" }, "config": { "allow-plugins": { diff --git a/composer.lock b/composer.lock index 202dd34..d48a949 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0b7fb803e22a45eb87e24172337208aa", + "content-hash": "004839935ba1e159da0282046c6f3e0d", "packages": [ { "name": "christian-riesen/base32", @@ -1207,12 +1207,81 @@ } ], "time": "2025-08-27T11:34:33+00:00" + }, + { + "name": "tobimori/kirby-seo", + "version": "2.0.0-alpha.12", + "source": { + "type": "git", + "url": "https://github.com/tobimori/kirby-seo.git", + "reference": "910024c55540be63b85d59cfb7152e65417ce6ee" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tobimori/kirby-seo/zipball/910024c55540be63b85d59cfb7152e65417ce6ee", + "reference": "910024c55540be63b85d59cfb7152e65417ce6ee", + "shasum": "" + }, + "require": { + "getkirby/composer-installer": "^1.2.1", + "php": ">=8.3.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.48", + "getkirby/cli": "^1.8.0", + "getkirby/cms": "^5.0.0", + "spatie/schema-org": "^3.23", + "tobimori/kirby-queues": "^1.0.0-beta.1" + }, + "suggest": { + "getkirby/cli": "Enable background processing support", + "spatie/schema-org": "Enable the Schema.org support", + "tobimori/kirby-queues": "Enable background processing support" + }, + "type": "kirby-plugin", + "extra": { + "kirby-cms-path": false + }, + "autoload": { + "psr-4": { + "tobimori\\Seo\\": "classes" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "proprietary" + ], + "authors": [ + { + "name": "Tobias Möritz", + "email": "tobias@moeritz.io" + } + ], + "description": "The default choice for SEO on Kirby: Implement technical SEO & Meta best practices with ease and provide an easy-to-use editor experience", + "homepage": "https://github.com/tobimori/kirby-seo#readme", + "support": { + "issues": "https://github.com/tobimori/kirby-seo/issues", + "source": "https://github.com/tobimori/kirby-seo/tree/2.0.0-alpha.12" + }, + "funding": [ + { + "url": "https://plugins.andkindness.com/seo/preorder", + "type": "custom" + }, + { + "url": "https://github.com/tobimori", + "type": "github" + } + ], + "time": "2026-01-21T11:45:12+00:00" } ], "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "tobimori/kirby-seo": 15 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/kirby/config/blocks/video/video.yml b/kirby/config/blocks/video/video.yml index b5fc104..270768e 100644 --- a/kirby/config/blocks/video/video.yml +++ b/kirby/config/blocks/video/video.yml @@ -29,12 +29,12 @@ fields: type: files query: model.images multiple: false + required: true image: back: black uploads: template: blocks/image - when: - location: kirby + help: Affichée si la vidéo ne charge pas. **Obligatoire pour la version print.** caption: label: field.blocks.video.caption type: writer diff --git a/site/blueprints/blocks/gallery.yml b/site/blueprints/blocks/gallery.yml new file mode 100644 index 0000000..5bfa0d9 --- /dev/null +++ b/site/blueprints/blocks/gallery.yml @@ -0,0 +1,35 @@ +name: Galerie (slider) +icon: dashboard +preview: gallery +fields: + images: + label: field.blocks.gallery.images.label + type: files + query: model.images + multiple: true + layout: cards + size: small + empty: field.blocks.gallery.images.empty + uploads: + template: blocks/image + image: + ratio: 1/1 + # ratio: + # label: field.blocks.image.ratio + # type: select + # placeholder: Auto + # width: 1/2 + # options: + # 1/1: "1:1" + # 16/9: "16:9" + # 10/8: "10:8" + # 21/9: "21:9" + # 7/5: "7:5" + # 4/3: "4:3" + # 5/3: "5:3" + # 3/2: "3:2" + # 3/1: "3:1" + # crop: + # label: field.blocks.image.crop + # type: toggle + # width: 1/2 diff --git a/site/blueprints/blocks/heading.yml b/site/blueprints/blocks/heading.yml index c17e29d..3c497f5 100644 --- a/site/blueprints/blocks/heading.yml +++ b/site/blueprints/blocks/heading.yml @@ -7,12 +7,18 @@ fields: label: field.blocks.heading.level type: toggles empty: false - default: "h2" + default: "h3" labels: false options: - value: h3 icon: h3 text: H3 + - value: h4 + icon: h4 + text: H4 + - value: h5 + icon: h5 + text: H5 text: label: field.blocks.heading.text type: writer diff --git a/site/blueprints/blocks/image.yml b/site/blueprints/blocks/image.yml index 392715d..1c9416c 100644 --- a/site/blueprints/blocks/image.yml +++ b/site/blueprints/blocks/image.yml @@ -7,21 +7,15 @@ fields: type: files query: model.images multiple: false + layout: cards + size: huge image: back: black uploads: template: blocks/image - alt: - label: field.blocks.image.alt - type: text - icon: title - help: Description de l’image, utile si elle ne s’affiche pas. Écrivez comme si vous expliquiez l'image à quelqu'un par téléphone. - caption: - label: field.blocks.image.caption - type: writer - icon: text - inline: true + help: Pensez à **ajouter le texte alternatif et la légende** en cliquant sur l'image ci-dessus link: label: field.blocks.image.link type: text icon: url + help: Accessible au clic sur l'image diff --git a/site/blueprints/blocks/text.yml b/site/blueprints/blocks/text.yml index 740ced6..be02ce1 100644 --- a/site/blueprints/blocks/text.yml +++ b/site/blueprints/blocks/text.yml @@ -5,6 +5,6 @@ preview: text fields: text: type: writer - headings: - - 4 + nodes: + - bulletList placeholder: field.blocks.text.placeholder diff --git a/site/blueprints/fields/investigations.yml b/site/blueprints/fields/investigations.yml new file mode 100644 index 0000000..f6c3c0b --- /dev/null +++ b/site/blueprints/fields/investigations.yml @@ -0,0 +1,9 @@ +label: Enquêtes +type: pages +parent: page('enquetes') +layout: cards +text: "{{ page.title }}, {{ page.subtitle }}" +info: "{{ page.incidentConsequences }} à {{ page.incidentLocation }}" +image: + cover: true + ratio: 16/9 diff --git a/site/blueprints/files/default.yml b/site/blueprints/files/default.yml new file mode 100644 index 0000000..71caf2d --- /dev/null +++ b/site/blueprints/files/default.yml @@ -0,0 +1,11 @@ +title: Fichier + +fields: + caption: + label: Légende + type: text + help: Légende affichée sous l'image + alt: + label: Texte alternatif + type: text + help: Description de l'image pour l'accessibilité et le SEO diff --git a/site/blueprints/pages/about.yml b/site/blueprints/pages/about.yml index 6f11445..f06fb49 100644 --- a/site/blueprints/pages/about.yml +++ b/site/blueprints/pages/about.yml @@ -1 +1,7 @@ title: À propos + +tabs: + contentTab: + label: Contenu + icon: page + seo: seo/page diff --git a/site/blueprints/pages/contact.yml b/site/blueprints/pages/contact.yml new file mode 100644 index 0000000..7a501e1 --- /dev/null +++ b/site/blueprints/pages/contact.yml @@ -0,0 +1,6 @@ +title: Contact + +tabs: + contentTab: + label: Contenu + seo: seo/page diff --git a/site/blueprints/pages/database.yml b/site/blueprints/pages/database.yml new file mode 100644 index 0000000..a406d88 --- /dev/null +++ b/site/blueprints/pages/database.yml @@ -0,0 +1,32 @@ +title: Bases de données + +options: + changeSlug: false + +columns: + - width: 1/3 + fields: + databaseInfo: + label: false + type: info + text: Ici sont stockées des données transversales, utilisées par différentes pages du site. + - width: 2/3 + fields: + countries: + label: Pays + type: tags + team: + label: Équipe + type: tags + keywords: + label: Mots-clés + type: tags + methodologies: + label: Méthodologies + type: tags + robots: + type: hidden + default: noindex, nofollow + sitemap: + type: hidden + default: false diff --git a/site/blueprints/pages/default.yml b/site/blueprints/pages/default.yml index 0cb0129..2772be0 100644 --- a/site/blueprints/pages/default.yml +++ b/site/blueprints/pages/default.yml @@ -1,21 +1,25 @@ title: Default Page -columns: - main: - width: 2/3 - sections: - fields: - type: fields - fields: - text: - type: textarea - size: huge - sidebar: - width: 1/3 - sections: - pages: - type: pages - template: default - files: - type: files - +tabs: + contentTab: + label: Contenu + icon: page + columns: + main: + width: 2/3 + sections: + fields: + type: fields + fields: + text: + type: textarea + size: huge + sidebar: + width: 1/3 + sections: + pages: + type: pages + template: default + files: + type: files + seo: seo/page diff --git a/site/blueprints/pages/home.yml b/site/blueprints/pages/home.yml index 06870fd..80acfda 100644 --- a/site/blueprints/pages/home.yml +++ b/site/blueprints/pages/home.yml @@ -1 +1,18 @@ title: Accueil + +tabs: + contentTab: + label: Contenu + icon: page + fields: + mainBaseline: + label: Baseline + type: text + help: À gauche des enquêtes. + width: 1/2 + impactsBaseline: + label: Phrase d'introduction des impacts + type: text + help: À gauche des impacts. + width: 1/2 + seo: seo/page diff --git a/site/blueprints/pages/impact.yml b/site/blueprints/pages/impact.yml new file mode 100644 index 0000000..b12a64e --- /dev/null +++ b/site/blueprints/pages/impact.yml @@ -0,0 +1,10 @@ +title: Impact + +tabs: + contentTab: + linkedInvestigations: + type: pages + template: investigation-summary + linkedMedias: + type: url + seo: seo/page diff --git a/site/blueprints/pages/impacts.yml b/site/blueprints/pages/impacts.yml index fad6765..f7ffd62 100644 --- a/site/blueprints/pages/impacts.yml +++ b/site/blueprints/pages/impacts.yml @@ -1 +1,6 @@ title: Impacts + +tabs: + contentTab: + label: Contenu + seo: seo/page diff --git a/site/blueprints/pages/investigation-summary.yml b/site/blueprints/pages/investigation-summary.yml index e69de29..69a4c34 100644 --- a/site/blueprints/pages/investigation-summary.yml +++ b/site/blueprints/pages/investigation-summary.yml @@ -0,0 +1,139 @@ +title: Investigation Summary + +tabs: + contentTab: + label: Contenu + icon: page + columns: + - width: 2/6 + sections: + createdSection: + type: fields + fields: + created: + label: Première publication + type: date + display: DD / MM / YYYY + default: today + width: 1/2 + cover: + label: Visuel de couverture + type: files + multiple: false + layout: cards + image: + ratio: 12/7 + cover: true + help: Image utilisée dans les listes d'enquêtes + reportSection: + label: Rapport + type: pages + template: report + - width: 4/6 + fields: + chapo: + label: Chapo + type: writer + nodes: false + buttons: false + heroType: + label: Type de hero + type: select + default: image + options: + image: Image simple + video: Vidéo + default: image + width: 1/3 + heroImages: + label: Images hero + type: files + multiple: true + layout: cards + width: 1/3 + when: + heroType: image + videoPreview: + label: Preview + type: files + multiple: false + help: Extrait joué en autoplay muet. + width: 1/3 + when: + heroType: video + videoUrl: + label: URL vidéo complète (YouTube embed) + type: url + width: 1/3 + when: + heroType: video + synthesis: + label: Synthèse + type: writer + size: large + headings: + - 4 + nodes: + - heading + - bulletlist + - orderedlist + marks: + - bold + - italic + - underline + - link + - '|' + - clear + metadataTab: + label: Métadonnées + icon: table + fields: + incidentDate: + label: Date de l'incident + type: date + display: DD / MM / YYYY + width: 1/4 + incidentLocation: + label: Lieu de l'incident + type: text + width: 1/4 + incidentCountry: + label: Pays de l'incident + type: multiselect + options: query + query: page('database').countries.split + width: 1/4 + incidentConsequences: + label: Conséquence(s) + type: text + width: 1/4 + keywords: + label: Mots-clés + type: multiselect + options: query + query: page('database').keywords.split + width: 1/4 + methodologies: + label: Méthodologies + options: query + query: page('database').methodologies.split + type: multiselect + width: 1/4 + partners: + label: Partenaires + type: structure + width: 1/4 + fields: + name: + label: Nom + type: text + link: + label: Lien + type: url + team: + label: Équipe Index + type: multiselect + options: query + query: page('database').team.split + width: 1/4 + seo: seo/page diff --git a/site/blueprints/pages/investigation.yml b/site/blueprints/pages/investigation.yml deleted file mode 100644 index ffa08e1..0000000 --- a/site/blueprints/pages/investigation.yml +++ /dev/null @@ -1,104 +0,0 @@ -title: Investigation -create: - fields: - - subtitle - -tabs: - contentTab: - label: Contenu - icon: page - columns: - - width: 2/6 - fields: - created: - label: Première publication - type: date - display: DD / MM / YYYY - default: today - width: 1/4 - - width: 4/6 - fields: - subtitle: - label: Sous-titre - type: text - chapo: - label: Chapo - type: writer - nodes: false - buttons: false - cover: - label: Visuel de couverture - type: files - multiple: false - min: 1 - layout: cards - size: full - image: - ratio: 12/7 - cover: true - body: - label: Corps - type: layout - layouts: - - "1/1" - - "1/2, 1/2" - - "1/3, 1/3, 1/3" - fieldsets: - - heading - - text - - image - metadataTab: - label: Métadonnées - icon: table - fields: - incidentDate: - label: Date de l'incident - type: date - display: DD / MM / YYYY - width: 1/3 - incidentLocation: - label: Lieu de l'incident - type: text - width: 1/3 - incidentConsequences: - label: Conséquence(s) - type: text - width: 1/3 - keywords: - label: Mots-clés - type: tags - width: 1/2 - partners: - label: Partenaires - type: structure - width: 1/2 - fields: - name: - label: Nom - type: text - link: - label: Lien - type: link - options: - - url - line: - type: line - indexTeam: - label: Équipe Index - type: structure - columns: - responsability: - width: 1/2 - names: - width: 1/2 - fields: - responsability: - label: Responsabilité - type: text - width: 1/2 - names: - label: Nom - type: entries - width: 1/2 - field: - type: text diff --git a/site/blueprints/pages/investigations.yml b/site/blueprints/pages/investigations.yml index 85a23c1..d2d73cc 100644 --- a/site/blueprints/pages/investigations.yml +++ b/site/blueprints/pages/investigations.yml @@ -4,13 +4,5 @@ tabs: contentTab: label: Contenu sections: - investigations: - type: pages - text: "{{ page.title }}, {{ page.subtitle }}" - info: "{{ page.incidentConsequences }} à {{ page.incidentLocation }}" - layout: cards - size: huge - search: true - image: - cover: true - ratio: 12/7 + investigations: fields/investigations + seo: seo/page diff --git a/site/blueprints/pages/laboratory.yml b/site/blueprints/pages/laboratory.yml new file mode 100644 index 0000000..72a6b68 --- /dev/null +++ b/site/blueprints/pages/laboratory.yml @@ -0,0 +1,6 @@ +title: Laboratoire + +tabs: + contentTab: + label: Contenu + seo: seo/page diff --git a/site/blueprints/pages/legal-notices.yml b/site/blueprints/pages/legal-notices.yml new file mode 100644 index 0000000..5ba6fbe --- /dev/null +++ b/site/blueprints/pages/legal-notices.yml @@ -0,0 +1,6 @@ +title: Mentions légales + +tabs: + contentTab: + label: Contenu + seo: seo/page diff --git a/site/blueprints/pages/package.yml b/site/blueprints/pages/package.yml new file mode 100644 index 0000000..53adfe5 --- /dev/null +++ b/site/blueprints/pages/package.yml @@ -0,0 +1,26 @@ +title: Dossier + +tabs: + contentTab: + label: Contenu + icon: page + fields: + cover: + label: Visuel de couverture + type: files + multiple: false + layout: cards + width: 1/3 + image: + ratio: 12/7 + cover: true + help: Image utilisée dans la liste des dossiers + description: + label: Description + type: writer + width: 2/3 + linkedContent: + label: Contenu + type: pages + query: site.find('enquetes').children + seo: seo/page diff --git a/site/blueprints/pages/packages.yml b/site/blueprints/pages/packages.yml new file mode 100644 index 0000000..668dc8e --- /dev/null +++ b/site/blueprints/pages/packages.yml @@ -0,0 +1,19 @@ +title: Dossiers + +tabs: + contentTab: + label: Contenu + sections: + packages: + label: Dossiers + type: pages + text: "{{ page.title }}" + info: "{{ page.linkedContent.toPages.count }} contenus" + layout: cards + size: huge + search: true + image: + cover: true + ratio: 12/7 + template: package + seo: seo/page diff --git a/site/blueprints/pages/product.yml b/site/blueprints/pages/product.yml index f39e50d..4351b14 100644 --- a/site/blueprints/pages/product.yml +++ b/site/blueprints/pages/product.yml @@ -27,3 +27,4 @@ tabs: type: entries field: type: text + seo: seo/page diff --git a/site/blueprints/pages/report.yml b/site/blueprints/pages/report.yml new file mode 100644 index 0000000..b7c1f9d --- /dev/null +++ b/site/blueprints/pages/report.yml @@ -0,0 +1,54 @@ +title: Investigation +create: + fields: + - subtitle + +tabs: + contentTab: + label: Contenu + icon: page + columns: + - width: 1/4 + fields: + created: + label: Première publication + type: date + display: DD / MM / YYYY + default: today + width: 1/2 + authors: + label: Auteur(s) + type: text + placeholder: INDEX Investigation + default: INDEX Investigation + cover: + label: Visuel de couverture + type: files + multiple: false + min: 1 + required: true + layout: cards + size: full + image: + ratio: 12/7 + cover: true + - width: 3/4 + fields: + subtitle: + label: Sous-titre + type: text + body: + label: Corps + type: layout + layouts: + - "1/1" + - "1/2, 1/2" + fieldsets: + - heading + - text + - image + - beforeafter + - video + # - horizontal-gallery + - gallery + seo: seo/page diff --git a/site/blueprints/pages/resources.yml b/site/blueprints/pages/resources.yml index 35ad676..c2bec4e 100644 --- a/site/blueprints/pages/resources.yml +++ b/site/blueprints/pages/resources.yml @@ -1 +1,7 @@ title: Resources + +tabs: + contentTab: + label: Contenu + icon: page + seo: seo/page diff --git a/site/blueprints/pages/shop.yml b/site/blueprints/pages/shop.yml index 37ea190..b197e28 100644 --- a/site/blueprints/pages/shop.yml +++ b/site/blueprints/pages/shop.yml @@ -8,3 +8,4 @@ tabs: type: pages template: product ratio: 4/3 + seo: seo/page diff --git a/site/blueprints/pages/support.yml b/site/blueprints/pages/support.yml index 8b6f16c..9d69b63 100644 --- a/site/blueprints/pages/support.yml +++ b/site/blueprints/pages/support.yml @@ -43,3 +43,4 @@ tabs: answer: label: Réponse type: text + seo: seo/page diff --git a/site/blueprints/site.yml b/site/blueprints/site.yml index b7da661..18aabdf 100644 --- a/site/blueprints/site.yml +++ b/site/blueprints/site.yml @@ -1,5 +1,8 @@ -title: Site +title: Dashboard -sections: - pages: - type: pages +tabs: + contentTab: + label: Contenu + sections: + investigations: fields/investigations + seo: seo/site diff --git a/site/config/config.php b/site/config/config.php index 8e65d31..df98960 100644 --- a/site/config/config.php +++ b/site/config/config.php @@ -2,6 +2,8 @@ return [ 'debug' => true, + 'languages' => true, + 'date.handler' => 'intl', 'thumbs' => [ 'quality' => 80, 'presets' => [ @@ -30,5 +32,6 @@ return [ 'campaign_slug' => 'soutenir-index-en-2024', 'campaign_url' => 'https://donorbox.org/soutenir-index-en-2024', 'api_base_url' => 'https://donorbox.org/api/v1' - ] + ], + 'tobimori.seo.canonicalBase' => 'https://www.index.ngo' ]; \ No newline at end of file diff --git a/site/config/menu.php b/site/config/menu.php index a3f33c2..d557ec6 100644 --- a/site/config/menu.php +++ b/site/config/menu.php @@ -1,23 +1,98 @@ [ - 'label' => 'Soutien', - 'icon' => 'money', - 'link' => 'pages/soutien', - 'current' => function ($current) { - $path = Kirby::instance()->request()->path()->toString(); - return Str::contains($path, 'pages/soutien'); - }, + 'dashboard' => [ + 'label' => 'Dashboard', + 'icon' => 'bolt', + 'link' => '/', + 'current' => function (string $current): bool { + $path = Kirby\Cms\App::instance()->path(); + return Str::contains($path, '/site'); + } ], - 'shop' => [ - 'label' => 'Boutique', - 'icon' => 'cart', - 'link' => 'pages/boutique', - 'current' => function ($current) { - $path = Kirby::instance()->request()->path()->toString(); - return Str::contains($path, 'pages/boutique'); - }, + '-', + 'home' => [ + 'label' => 'Accueil', + 'icon' => 'home', + 'link' => 'pages/home', + 'current' => function (string $current): bool { + $path = Kirby\Cms\App::instance()->path(); + return Str::contains($path, 'pages/home'); + } + ], + 'enquetes' => [ + 'label' => 'Enquêtes', + 'icon' => 'pen', + 'link' => 'pages/enquetes', + 'current' => function (string $current): bool { + $path = Kirby\Cms\App::instance()->path(); + return Str::contains($path, 'pages/enquetes'); + } + ], + 'impacts' => [ + 'label' => 'Impacts', + 'icon' => 'globe', + 'link' => 'pages/impacts', + 'current' => function (string $current): bool { + $path = Kirby\Cms\App::instance()->path(); + return Str::contains($path, 'pages/impacts'); + } + ], + 'dossiers' => [ + 'label' => 'Dossiers', + 'icon' => 'folder', + 'link' => 'pages/dossiers', + 'current' => function (string $current): bool { + $path = Kirby\Cms\App::instance()->path(); + return Str::contains($path, 'pages/dossiers'); + } + ], + 'laboratoire' => [ + 'label' => 'Laboratoire', + 'icon' => 'attachment', + 'link' => 'pages/laboratoire', + 'current' => function (string $current): bool { + $path = Kirby\Cms\App::instance()->path(); + return Str::contains($path, 'pages/laboratoire'); + } + ], + '-', + 'contact' => [ + 'label' => 'Contact', + 'icon' => 'email', + 'link' => 'pages/contact', + 'current' => function (string $current): bool { + $path = Kirby\Cms\App::instance()->path(); + return Str::contains($path, 'pages/contact'); + } + ], + 'a-propos' => [ + 'label' => 'À propos', + 'icon' => 'question', + 'link' => 'pages/a-propos', + 'current' => function (string $current): bool { + $path = Kirby\Cms\App::instance()->path(); + return Str::contains($path, 'pages/a-propos'); + } + ], + 'legal-notices' => [ + 'label' => 'Mentions légales', + 'icon' => 'email', + 'link' => 'pages/mentions-legales', + 'current' => function (string $current): bool { + $path = Kirby\Cms\App::instance()->path(); + return Str::contains($path, 'pages/mentions-legales'); + } + ], + '-', + 'database' => [ + 'label' => 'Bases de données', + 'icon' => 'table', + 'link' => 'pages/database', + 'current' => function (string $current): bool { + $path = Kirby\Cms\App::instance()->path(); + return Str::contains($path, 'pages/database'); + } ], '-', 'users' diff --git a/site/languages/en.php b/site/languages/en.php new file mode 100644 index 0000000..8441bf0 --- /dev/null +++ b/site/languages/en.php @@ -0,0 +1,9 @@ + 'en', + 'default' => false, + 'direction' => 'ltr', + 'locale' => 'en_US.UTF-8', + 'name' => 'English', + 'url' => '/en', +]; \ No newline at end of file diff --git a/site/languages/fr.php b/site/languages/fr.php new file mode 100644 index 0000000..77fc6a6 --- /dev/null +++ b/site/languages/fr.php @@ -0,0 +1,10 @@ + 'fr', + 'default' => true, + 'direction' => 'ltr', + 'locale' => 'fr_FR.UTF-8', + 'name' => 'Français', + 'url' => '/', +]; \ No newline at end of file diff --git a/site/plugins/beforeafter/.gitignore b/site/plugins/beforeafter/.gitignore new file mode 100644 index 0000000..552f221 --- /dev/null +++ b/site/plugins/beforeafter/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +*.log diff --git a/site/plugins/beforeafter/blueprints/blocks/beforeafter.yml b/site/plugins/beforeafter/blueprints/blocks/beforeafter.yml new file mode 100644 index 0000000..74e36ac --- /dev/null +++ b/site/plugins/beforeafter/blueprints/blocks/beforeafter.yml @@ -0,0 +1,23 @@ +name: Comparaison Avant/Après +icon: images +preview: beforeafter +fields: + imageBefore: + label: Image "Avant" + type: files + multiple: false + layout: cards + query: page.images + help: Image affichée à gauche / dessous + width: 1/2 + imageAfter: + label: Image "Après" + type: files + multiple: false + layout: cards + query: page.images + help: Image affichée à droite / dessus + width: 1/2 + caption: + label: Légende + type: text diff --git a/site/plugins/beforeafter/index.css b/site/plugins/beforeafter/index.css new file mode 100644 index 0000000..529d888 --- /dev/null +++ b/site/plugins/beforeafter/index.css @@ -0,0 +1 @@ +.beforeafter-preview[data-v-7994b7b1]{cursor:pointer;border-radius:var(--rounded);overflow:hidden;background:var(--color-background);border:1px solid var(--color-gray-300)}.beforeafter-preview__slider[data-v-7994b7b1]{position:relative;width:100%;height:200px;background:var(--color-gray-200)}.beforeafter-preview__image[data-v-7994b7b1]{position:absolute;top:0;left:0;width:100%;height:100%;object-fit:cover}.beforeafter-preview__image--before[data-v-7994b7b1]{clip-path:polygon(0 0,50% 0,50% 100%,0 100%)}.beforeafter-preview__divider[data-v-7994b7b1]{position:absolute;left:50%;top:0;bottom:0;width:2px;background:#fff;box-shadow:0 0 8px #0000004d;transform:translate(-1px)}.beforeafter-preview__caption[data-v-7994b7b1]{padding:.75rem;font-size:var(--text-sm);color:var(--color-gray-600);font-style:italic;background:var(--color-background);margin:0}.beforeafter-preview__empty[data-v-7994b7b1]{padding:3rem 1rem;text-align:center;color:var(--color-gray-500);font-size:var(--text-sm);background:var(--color-gray-100)}.beforeafter-preview[data-v-7994b7b1]:hover{border-color:var(--color-gray-400)} diff --git a/site/plugins/beforeafter/index.js b/site/plugins/beforeafter/index.js new file mode 100644 index 0000000..3116220 --- /dev/null +++ b/site/plugins/beforeafter/index.js @@ -0,0 +1 @@ +(function(){"use strict";function f(a,e,r,t,n,o,u,p){var i=typeof a=="function"?a.options:a;return e&&(i.render=e,i.staticRenderFns=r,i._compiled=!0),i._scopeId="data-v-"+o,{exports:a,options:i}}const s={__name:"BeforeAfterBlock",props:{content:Object},setup(a){const e=a,r=Vue.computed(()=>{var o;if(!((o=e.content)!=null&&o.imagebefore)||!e.content.imagebefore.length)return null;const n=e.content.imagebefore[0];return(n==null?void 0:n.url)||null}),t=Vue.computed(()=>{var o;if(!((o=e.content)!=null&&o.imageafter)||!e.content.imageafter.length)return null;const n=e.content.imageafter[0];return(n==null?void 0:n.url)||null});return{__sfc:!0,props:e,imageBeforeUrl:r,imageAfterUrl:t}}};var c=function(){var e=this,r=e._self._c,t=e._self._setupProxy;return r("div",{staticClass:"beforeafter-preview",on:{click:function(n){return e.$emit("open")}}},[t.imageBeforeUrl||t.imageAfterUrl?r("div",{staticClass:"beforeafter-preview__slider"},[t.imageAfterUrl?r("img",{staticClass:"beforeafter-preview__image beforeafter-preview__image--after",attrs:{src:t.imageAfterUrl,alt:"Après"}}):e._e(),t.imageBeforeUrl?r("img",{staticClass:"beforeafter-preview__image beforeafter-preview__image--before",attrs:{src:t.imageBeforeUrl,alt:"Avant"}}):e._e(),t.imageBeforeUrl&&t.imageAfterUrl?r("div",{staticClass:"beforeafter-preview__divider"}):e._e()]):e._e(),e.content.caption?r("p",{staticClass:"beforeafter-preview__caption"},[e._v(" "+e._s(e.content.caption)+" ")]):e._e(),!t.imageBeforeUrl&&!t.imageAfterUrl?r("div",{staticClass:"beforeafter-preview__empty"},[e._v(" Cliquer pour ajouter des images ")]):e._e()])},l=[],_=f(s,c,l,!1,null,"7994b7b1");const m=_.exports;window.panel.plugin("index/beforeafter",{blocks:{beforeafter:m}})})(); diff --git a/site/plugins/beforeafter/index.php b/site/plugins/beforeafter/index.php new file mode 100644 index 0000000..9a9b1bb --- /dev/null +++ b/site/plugins/beforeafter/index.php @@ -0,0 +1,10 @@ + [ + 'blocks/beforeafter' => __DIR__ . '/blueprints/blocks/beforeafter.yml' + ], + 'snippets' => [ + 'blocks/beforeafter' => __DIR__ . '/snippets/blocks/beforeafter.php' + ] +]); diff --git a/site/plugins/beforeafter/package.json b/site/plugins/beforeafter/package.json new file mode 100644 index 0000000..df2f9cb --- /dev/null +++ b/site/plugins/beforeafter/package.json @@ -0,0 +1,12 @@ +{ + "name": "beforeafter-block", + "version": "1.0.0", + "scripts": { + "dev": "npx -y kirbyup src/index.js --watch", + "build": "npx -y kirbyup src/index.js" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.1", + "vite": "^7.1.7" + } +} diff --git a/site/plugins/beforeafter/snippets/blocks/beforeafter.php b/site/plugins/beforeafter/snippets/blocks/beforeafter.php new file mode 100644 index 0000000..8718c07 --- /dev/null +++ b/site/plugins/beforeafter/snippets/blocks/beforeafter.php @@ -0,0 +1,98 @@ +imageBefore()->toFile(); +$imageAfter = $block->imageAfter()->toFile(); +$caption = $block->caption()->value(); +?> + + +
+
+ + <?= $imageBefore->alt()->or('Image avant')->esc() ?> + + + + <?= $imageAfter->alt()->or('Image après')->esc() ?> + +
+ + + + +
+ + +

+ + diff --git a/site/plugins/beforeafter/src/components/BeforeAfterBlock.vue b/site/plugins/beforeafter/src/components/BeforeAfterBlock.vue new file mode 100644 index 0000000..cca45bc --- /dev/null +++ b/site/plugins/beforeafter/src/components/BeforeAfterBlock.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/site/plugins/beforeafter/src/index.js b/site/plugins/beforeafter/src/index.js new file mode 100644 index 0000000..bb9d8e2 --- /dev/null +++ b/site/plugins/beforeafter/src/index.js @@ -0,0 +1,7 @@ +import BeforeAfterBlock from "./components/BeforeAfterBlock.vue"; + +window.panel.plugin("index/beforeafter", { + blocks: { + beforeafter: BeforeAfterBlock + } +}); diff --git a/site/plugins/horizontal-gallery/.gitignore b/site/plugins/horizontal-gallery/.gitignore new file mode 100644 index 0000000..552f221 --- /dev/null +++ b/site/plugins/horizontal-gallery/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +*.log diff --git a/site/plugins/horizontal-gallery/blueprints/blocks/horizontal-gallery.yml b/site/plugins/horizontal-gallery/blueprints/blocks/horizontal-gallery.yml new file mode 100644 index 0000000..af8d8a9 --- /dev/null +++ b/site/plugins/horizontal-gallery/blueprints/blocks/horizontal-gallery.yml @@ -0,0 +1,22 @@ +name: Galerie horizontale +icon: images +preview: horizontal-gallery +fields: + images: + label: Images + type: files + multiple: true + layout: cards + query: page.images + help: Pensez à ajouter une légende à chaque image via son blueprint (champ Caption) + text: + label: Texte accompagnant + type: writer + marks: + - bold + - italic + - link + nodes: + - bulletList + - orderedList + - paragraph diff --git a/site/plugins/horizontal-gallery/index.css b/site/plugins/horizontal-gallery/index.css new file mode 100644 index 0000000..a844110 --- /dev/null +++ b/site/plugins/horizontal-gallery/index.css @@ -0,0 +1 @@ +.hgallery-preview[data-v-9cf511cf]{cursor:pointer;border-radius:var(--rounded);overflow:hidden;background:var(--color-background);border:1px solid var(--color-gray-300)}.hgallery-preview__container[data-v-9cf511cf]{display:flex;flex-direction:column;gap:1rem;padding:1rem}.hgallery-preview__gallery[data-v-9cf511cf]{width:100%}.hgallery-preview__scroll[data-v-9cf511cf]{display:flex;gap:1rem;overflow-x:auto;padding-bottom:.5rem;scroll-behavior:smooth}.hgallery-preview__scroll[data-v-9cf511cf]::-webkit-scrollbar{height:6px}.hgallery-preview__scroll[data-v-9cf511cf]::-webkit-scrollbar-track{background:var(--color-gray-200);border-radius:3px}.hgallery-preview__scroll[data-v-9cf511cf]::-webkit-scrollbar-thumb{background:var(--color-gray-400);border-radius:3px}.hgallery-preview__scroll[data-v-9cf511cf]::-webkit-scrollbar-thumb:hover{background:var(--color-gray-500)}.hgallery-preview__slide[data-v-9cf511cf]{flex-shrink:0;width:250px;display:flex;flex-direction:column;gap:.5rem}.hgallery-preview__slide img[data-v-9cf511cf]{width:100%;height:180px;object-fit:cover;border-radius:var(--rounded-sm);background:var(--color-gray-200)}.hgallery-preview__caption[data-v-9cf511cf]{font-size:var(--text-xs);color:var(--color-gray-600);font-style:italic;margin:0;line-height:1.4}.hgallery-preview__empty[data-v-9cf511cf]{padding:3rem 1rem;text-align:center;color:var(--color-gray-500);font-size:var(--text-sm);background:var(--color-gray-100);border-radius:var(--rounded-sm)}.hgallery-preview__text[data-v-9cf511cf]{font-size:var(--text-sm);color:var(--color-gray-700);line-height:1.5;padding-top:.5rem;border-top:1px solid var(--color-gray-200)}.hgallery-preview[data-v-9cf511cf]:hover{border-color:var(--color-gray-400)} diff --git a/site/plugins/horizontal-gallery/index.js b/site/plugins/horizontal-gallery/index.js new file mode 100644 index 0000000..4106ff9 --- /dev/null +++ b/site/plugins/horizontal-gallery/index.js @@ -0,0 +1 @@ +(function(){"use strict";function r(l,e,t,a,n,s,v,d){var i=typeof l=="function"?l.options:l;return e&&(i.render=e,i.staticRenderFns=t,i._compiled=!0),i._scopeId="data-v-"+s,{exports:l,options:i}}const o={__name:"HorizontalGalleryBlock",props:{content:Object},setup(l){const e=l,t=Vue.computed(()=>{var a;return!((a=e.content)!=null&&a.images)||!e.content.images.length?[]:e.content.images.map(n=>({url:n.url,caption:n.text||n.caption||null}))});return{__sfc:!0,props:e,images:t}}};var c=function(){var e=this,t=e._self._c,a=e._self._setupProxy;return t("div",{staticClass:"hgallery-preview",on:{click:function(n){return e.$emit("open")}}},[t("div",{staticClass:"hgallery-preview__container"},[t("div",{staticClass:"hgallery-preview__gallery"},[a.images.length>0?t("div",{staticClass:"hgallery-preview__scroll"},e._l(a.images,function(n,s){return t("div",{key:s,staticClass:"hgallery-preview__slide"},[t("img",{attrs:{src:n.url,alt:n.caption||"Image"}}),n.caption?t("p",{staticClass:"hgallery-preview__caption"},[e._v(" "+e._s(n.caption)+" ")]):e._e()])}),0):t("div",{staticClass:"hgallery-preview__empty"},[e._v(" Aucune image sélectionnée ")])]),e.content.text?t("div",{staticClass:"hgallery-preview__text"},[t("div",{domProps:{innerHTML:e._s(e.content.text)}})]):e._e()])])},_=[],p=r(o,c,_,!1,null,"9cf511cf");const u=p.exports;window.panel.plugin("index/horizontal-gallery",{blocks:{"horizontal-gallery":u}})})(); diff --git a/site/plugins/horizontal-gallery/index.php b/site/plugins/horizontal-gallery/index.php new file mode 100644 index 0000000..f7ea19b --- /dev/null +++ b/site/plugins/horizontal-gallery/index.php @@ -0,0 +1,10 @@ + [ + 'blocks/horizontal-gallery' => __DIR__ . '/blueprints/blocks/horizontal-gallery.yml' + ], + 'snippets' => [ + 'blocks/horizontal-gallery' => __DIR__ . '/snippets/blocks/horizontal-gallery.php' + ] +]); diff --git a/site/plugins/horizontal-gallery/package.json b/site/plugins/horizontal-gallery/package.json new file mode 100644 index 0000000..6170309 --- /dev/null +++ b/site/plugins/horizontal-gallery/package.json @@ -0,0 +1,12 @@ +{ + "name": "horizontal-gallery-block", + "version": "1.0.0", + "scripts": { + "dev": "npx -y kirbyup src/index.js --watch", + "build": "npx -y kirbyup src/index.js" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.1", + "vite": "^7.1.7" + } +} diff --git a/site/plugins/horizontal-gallery/snippets/blocks/horizontal-gallery.php b/site/plugins/horizontal-gallery/snippets/blocks/horizontal-gallery.php new file mode 100644 index 0000000..846a436 --- /dev/null +++ b/site/plugins/horizontal-gallery/snippets/blocks/horizontal-gallery.php @@ -0,0 +1,38 @@ +images()->toFiles(); +$text = $block->text()->value(); +?> + +
+ +
+ +
+
+ +
+
+
+ <?= $image->alt()->esc() ?> +
+ caption()->isNotEmpty()): ?> +

caption()->html() ?>

+ +
+
+ +
+ +
+
+
+
+ + +
+ +
+ + +
diff --git a/site/plugins/horizontal-gallery/src/components/HorizontalGalleryBlock.vue b/site/plugins/horizontal-gallery/src/components/HorizontalGalleryBlock.vue new file mode 100644 index 0000000..a7d66e9 --- /dev/null +++ b/site/plugins/horizontal-gallery/src/components/HorizontalGalleryBlock.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/site/plugins/horizontal-gallery/src/index.js b/site/plugins/horizontal-gallery/src/index.js new file mode 100644 index 0000000..43f4870 --- /dev/null +++ b/site/plugins/horizontal-gallery/src/index.js @@ -0,0 +1,7 @@ +import HorizontalGalleryBlock from "./components/HorizontalGalleryBlock.vue"; + +window.panel.plugin("index/horizontal-gallery", { + blocks: { + "horizontal-gallery": HorizontalGalleryBlock + } +}); diff --git a/site/plugins/kirby-seo/LICENSE.md b/site/plugins/kirby-seo/LICENSE.md new file mode 100644 index 0000000..ad5f823 --- /dev/null +++ b/site/plugins/kirby-seo/LICENSE.md @@ -0,0 +1,132 @@ +# Plugin License Agreement + +Source: https://plugins.andkindness.com/license-agreement + +While most of our plugins source code are publicly available, they are, unless specified otherwise, not free software. To use any plugin in production, you need to purchase a license. + +## Summary + +> This is a legally non-binding summary. Please review the full license text carefully before using the plugins. + +This is a legal agreement between you (the customer) and Love & Kindness GmbH for using their Kirby CMS plugins. By downloading or using the plugins, or by purchasing a license, you agree to these terms: + +### What You Can Do + +- **Install and use the plugin on one website or multi-language website per license purchase** + If you need a separate Kirby CMS license for your site, you'll most likely also need a separate plugin license +- **Make copies of the plugin for backup or development purposes** +- **Modify the source code for your own use** + +### What You Cannot Do + +- **Use the plugin on additional websites without buying additional licenses** +- **Redistribute or resell the plugin or your modified versions** + +### Support & Updates + +- **Free updates for minor/patch releases, paid upgrades for major releases possible** +- **Email support provided for active license holders** + +--- + +This license is a legal agreement between **You** and **Love & Kindness GmbH, Beimoorstr. 20, 22081 Hamburg, Germany** (therein "**Our**"/"**We**"/"**Us**") for the use of any Kirby CMS plugins and resources (the "**Plugin**") created by Us and sold via Paddle.com. By downloading any Plugin files or resources or purchasing a license to the Plugin, you agree to be bound by the terms and conditions of this license. + +## Permitted Use + +This agreement grants a license for each purchase to install and use a single instance of the Plugin on a **specific website** limited by **its domain & subdomain**. If You use the cross-domain multi-language feature with the same `content` folder, these domains count as the same Website. + +Additional Plugin licenses must be purchased in order to install and use the Plugin on **additional websites**. + +The license is **non-exclusive** and **generally non-transferable**. + +A license is valid for all minor & patch updates of the Plugin (e.g. 1.0.x to 1.1.x). We reserve the right to charge an **upgrade fee for major updates** (e.g. 1.x.x to 2.x.x). Whether a release is a patch, minor, or major release is at Our sole discretion. + +## Development Usage + +You are permitted to install and use the Plugin on a personal computer (such as a desktop PC, notebook, or tablet) or a server, free of charge, for as long as necessary during the development stage. + +Any website that is used **purely for the purposes of development and client preview** is considered development usage. It must only be accessible by a restricted number of users. A website with **the intention to handle production data** is never considered development usage, no matter if the related website is publicly accessible or not. + +## Refund Policy + +We offer refunds on the Plugin within **14 days of purchase**. Contact support@andkindness.com for assistance. + +## Technical Support + +Technical support is available via email for active license owners. + +No support is provided for free plugins or plugins that are available both for free and paid usage. We do not provide phone support. No representations or guarantees are made regarding the response time in which support questions are answered, but we will do our very best to respond quickly. + +## All Rights Reserved + +Love & Kindness GmbH **owns all rights**, title and interest to the Plugin (including all intellectual property rights) and **reserves all rights to the Plugin** that are not expressly granted in this Agreement. + +## Restrictions + +### Making Copies + +You may make **copies of the Plugin** in any machine readable form solely for purposes of **deploying a website to a server, developing a website on a personal computer or server or as a backup**, provided that You reproduce the Plugin in its original form and with all proprietary notices on the copy. + +You may not reproduce the Plugin or its source code, in whole or in part, for **any other purpose**. + +### Modification of the Source Code + +You may **alter, modify or extend the source code** for Your own use. You may also **commission a third party** to perform those modifications for You. + +However You may not **alter or circumvent the licensing features**, including (but not limited to) the license validation or **resell, redistribute or transfer** the modified or derivative version. + +### Ownership and Intellectual Property + +The Plugin is copyrighted by Us. All rights not expressly granted to You are retained by Us, including intellectual property rights. + +### Disclaimer of Warranty + +THE PLUGIN IS OFFERED ON AN **"AS-IS" BASIS** AND **NO WARRANTY**, EITHER EXPRESSED OR IMPLIED, IS GIVEN. WE EXPRESSLY DISCLAIM ALL WARRANTIES OF ANY KIND, WHETHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. YOU ASSUME ALL RISK ASSOCIATED WITH THE QUALITY, PERFORMANCE, INSTALLATION AND USE OF THE PLUGIN INCLUDING, BUT NOT LIMITED TO, THE RISKS OF PROGRAM ERRORS, DAMAGE TO EQUIPMENT, LOSS OF DATA OR SOFTWARE PROGRAMS, OR UNAVAILABILITY OR INTERRUPTION OF OPERATIONS. **YOU ARE SOLELY RESPONSIBLE** FOR DETERMINING THE APPROPRIATENESS OF USE OF THE PLUGIN AND ASSUME ALL RISKS ASSOCIATED WITH ITS USE. THIS PARAGRAPH ALSO APPLIES TO YOU IF YOU ARE NOT THE LICENSEE (E.G. IF YOU USE THE PLUGIN WHILE SOMEONE ELSE IS THE LICENSEE). + +### Limitations of Liability + +YOU EXPRESSLY UNDERSTAND AND AGREE THAT **WE SHALL NOT BE LIABLE** FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL OR EXEMPLARY DAMAGES, INCLUDING BUT NOT LIMITED TO, DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA OR OTHER INTANGIBLE LOSSES (EVEN IF WE HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES). SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF THE LIMITATION OR EXCLUSION OF LIABILITY FOR INCIDENTAL OR CONSEQUENTIAL DAMAGES. ACCORDINGLY, **SOME OF THE ABOVE LIMITATIONS MAY NOT APPLY TO YOU**. **IN NO EVENT WILL OUR TOTAL CUMULATIVE DAMAGES EXCEED** THE FEES YOU PAID TO US UNDER THIS AGREEMENT IN THE MOST RECENT TWELVE-MONTH PERIOD. THIS PARAGRAPH ALSO APPLIES TO YOU IF YOU ARE NOT THE LICENSEE (E.G. IF YOU USE THE PLUGIN WHILE SOMEONE ELSE IS THE LICENSEE). + +## Termination + +The License may be terminated by either party if terms are breached and not remedied within a specified period. + +## Governing Law + +Any legal disputes that arise from or relate to this Agreement shall be exclusively resolved in the courts located in Hamburg, Germany. Nonetheless, we reserve the right to initiate legal proceedings against you in the jurisdiction where your principal place of business is situated. + +Should you be domiciled in Germany, the stipulations of the first paragraph will be relevant only if you are a businessperson, a public law entity, or a special fund under public law. + +If your domicile is not in Germany but within another member state of the European Union, the provisions of the first paragraph will apply to you only if you do not qualify as a consumer as defined under Article 17 of Regulation (EU) No. 1215/2012. In such circumstances, you may bring a legal action against us either in the jurisdiction of our place of business or where you habitually reside. Conversely, we are entitled to sue you exclusively in the courts of the member state where you have your domicile. + +In the event that your residence is outside Germany and not within any European Union member state, the provisions of the first paragraph are fully applicable without any modification. + +## Severability Clause + +Should any provision of this Agreement be or become invalid, void or unenforceable, in whole or in part, at present or in the future, this shall not affect the validity of the remaining provisions of this Agreement. The same shall apply if a gap requiring supplementation arises after conclusion of this Agreement. The parties shall replace the invalid, void or unenforceable provision or gap requiring filling by a valid provision which in its legal or economic content takes account of the invalid, void provision and the overall content of the agreement. § 139 BGB (partial invalidity) is expressly waived. + +--- + +Kirby SEO 1.x releases were previously licensed under the MIT License. + +MIT License + +Copyright (c) 2023-2024 Tobias Möritz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/site/plugins/kirby-seo/README.md b/site/plugins/kirby-seo/README.md new file mode 100644 index 0000000..2baf05d --- /dev/null +++ b/site/plugins/kirby-seo/README.md @@ -0,0 +1,52 @@ +![Kirby SEO Banner](/.github/new-banner.png) + +

Kirby SEO

+

+ The default choice for SEO on Kirby: Implement technical SEO & Meta best practices with ease and provide an easy-to-use editor experience +

+ +--- + +## Features + +- 🔎 All-in-one SEO and meta solution +- 🪜 The Meta Cascade: Intelligently merge metadata from multiple sources +- 🎛 Completely configurable: Disable features you don't need +- 💻 Simple Panel UI with previews for Google, Twitter, Facebook & Co. +- 📮 [Schema.org (JSON-LD)](https://schema.org/) support with fluent classes +- 🤖 Automatic Robots rule generation, based on page status +- 📝 Sitemap generation with multi-lang support + +### New in Version 2 + +- 🚀 Kirby 5 support +- 💻 Even better and easier Panel UI +- ✨ AI assist for writing meta tags +- 🔘 IndexNow support + +## Get started + +[Read the documentation](https://plugins.andkindness.com/seo/docs/get-started/feature-overview) to get started with Kirby SEO. + +If you're looking to use Kirby SEO with Kirby 5 or newer, please install the Alpha version of the plugin: + +`composer require tobimori/kirby-seo:^2.0.0-alpha.8` + +### What does Alpha mean for Kirby SEO 2? + +The core features of Kirby SEO, such as the meta cascade, the panel setup, sitemap and robots are stable and can be used in production. New features of v2 might be unstable or can occur breaking changes until the final release. + +## Contributing + +Kirby SEO is open to contributors: If you open a pull request that gets merged, such as fixing a bug or translating the plugin into a new language, you're eligible for a free SEO license of your choice. Please note that I might reject minor repeat contributions or simple fixes of typos for this. Please send an email to support after your contribution has been merged. + +## License + +Kirby SEO 2.0 is not free software. In order to run it on a public server, you'll have to purchase a valid Kirby license & a valid SEO license. +**The plugin is currently free to use while in pre-release state.** You can [pre-order a license](https://plugins.andkindness.com/seo/preorder) with a 20% discount for a limited time. + +Copyright 2023-2025 © Tobias Möritz - Love & Kindness GmbH + +--- + +[Kirby SEO 1.0 is licensed under the MIT license.](./LICENSE.md) diff --git a/site/plugins/kirby-seo/blueprints/fields/meta-group.yml b/site/plugins/kirby-seo/blueprints/fields/meta-group.yml new file mode 100644 index 0000000..7b9bb60 --- /dev/null +++ b/site/plugins/kirby-seo/blueprints/fields/meta-group.yml @@ -0,0 +1,34 @@ +type: group +fields: + _metaHeadline: + label: seo.page.meta.headline + type: headline + numbered: false + metaTitle: + label: seo.fields.titleOverwrite.label + type: seo-writer + ai: title + placeholder: "{{ page.title }}" + metaTemplate: + extends: seo/fields/title-template + label: seo.fields.metaTitleTemplate.label + help: seo.fields.metaTitleTemplate.help + width: 2/3 + placeholder: "{{ page.metadata.metaTemplate }}" + useTitleTemplate: + label: seo.fields.useTitleTemplate.label + type: toggle + help: seo.fields.useTitleTemplate.help + width: 1/3 + default: true + text: + - "{{ t('seo.fields.useTitleTemplate.no') }}" + - "{{ t('seo.fields.useTitleTemplate.yes') }}" + metaDescription: + label: seo.fields.metaDescription.label + type: seo-writer + ai: description + help: seo.fields.metaDescription.help + placeholder: "{{ page.metadata.metaDescription }}" + _seoLine1: + type: line diff --git a/site/plugins/kirby-seo/blueprints/fields/og-group.yml b/site/plugins/kirby-seo/blueprints/fields/og-group.yml new file mode 100644 index 0000000..3689e8e --- /dev/null +++ b/site/plugins/kirby-seo/blueprints/fields/og-group.yml @@ -0,0 +1,42 @@ +type: group +fields: + _ogHeadline: + label: seo.page.og.headline + type: headline + numbered: false + help: seo.site.og.headline.help + ogTemplate: + extends: seo/fields/title-template + label: seo.fields.ogTitleTemplate.label + width: 2/3 + help: seo.fields.metaTitleTemplate.help + placeholder: "{{ page.metadata.ogTemplate }}" + useOgTemplate: + label: seo.fields.useTitleTemplate.label + type: toggle + help: seo.fields.useTitleTemplate.help + width: 1/3 + default: true + text: + - "{{ t('seo.fields.useTitleTemplate.no') }}" + - "{{ t('seo.fields.useTitleTemplate.yes') }}" + ogDescription: + label: seo.fields.ogDescription.label + type: seo-writer + ai: og-description + placeholder: "{{ page.metadata.ogDescription }}" + ogImage: + label: seo.fields.ogImage.label + extends: seo/fields/og-image + empty: seo.fields.ogImage.empty + cropOgImage: + label: seo.fields.cropOgImage.label + type: select + width: 1/1 + placeholder: "{{ t('seo.common.default') }} {{ site.cropOgImage.toBool ? t('seo.common.yes') : t('seo.common.no') }}" + options: + "true": "{{ t('seo.common.yes') }}" + "false": "{{ t('seo.common.no') }}" + help: seo.fields.cropOgImage.help + _seoLine2: + type: line diff --git a/site/plugins/kirby-seo/blueprints/fields/og-image.php b/site/plugins/kirby-seo/blueprints/fields/og-image.php new file mode 100644 index 0000000..372a62d --- /dev/null +++ b/site/plugins/kirby-seo/blueprints/fields/og-image.php @@ -0,0 +1,31 @@ + 'files', + 'multiple' => false, + 'uploads' => [], + 'query' => 'model.images' + ]; + + if ($parent = option('tobimori.seo.files.parent')) { + $blueprint['uploads'] = [ + 'parent' => $parent + ]; + $blueprint['query'] = "{$parent}.images"; + } + + if ($template = option('tobimori.seo.files.template')) { + $blueprint['uploads'] = [ + ...$blueprint['uploads'], + 'template' => $template + ]; + + $blueprint['query'] = "{$blueprint['query']}.filterBy('template', '{$template}')"; + } + + return $blueprint; +}; diff --git a/site/plugins/kirby-seo/blueprints/fields/robots.php b/site/plugins/kirby-seo/blueprints/fields/robots.php new file mode 100644 index 0000000..b3c649c --- /dev/null +++ b/site/plugins/kirby-seo/blueprints/fields/robots.php @@ -0,0 +1,56 @@ + 'hidden' + ]; + } + + $fields = [ + '_robotsHeadline' => [ + 'label' => 'seo.fields.robots.label', + 'type' => 'headline', + 'numbered' => false, + ] + ]; + + $page = Meta::currentPage(); + foreach ($kirby->option('tobimori.seo.robots.types') as $robots) { + $upper = Str::ucfirst($robots); + + $fields["robots{$upper}"] = [ + 'label' => "seo.fields.robots.{$robots}.label", + 'type' => 'toggles', + 'help' => "seo.fields.robots.{$robots}.help", + 'width' => '1/2', + 'default' => 'default', + 'reset' => false, + 'options' => [ + 'default' => $page ? + A::join([ + t('seo.common.default'), + $page->metadata()->get("robots{$upper}", ['fields'])->toBool() ? t('seo.common.yes') : t('seo.common.no') + ], ' ') + : t('seo.common.default'), + 'true' => t('seo.common.yes'), + 'false' => t('seo.common.no'), + ] + ]; + } + + $fields['_seoLine3'] = [ + 'type' => 'line' + ]; + + return [ + 'type' => 'group', + 'fields' => $fields, + ]; +}; diff --git a/site/plugins/kirby-seo/blueprints/fields/site-robots.php b/site/plugins/kirby-seo/blueprints/fields/site-robots.php new file mode 100644 index 0000000..55d5b09 --- /dev/null +++ b/site/plugins/kirby-seo/blueprints/fields/site-robots.php @@ -0,0 +1,49 @@ +option('tobimori.seo.robots.active') || !$kirby->option('tobimori.seo.robots.pageSettings')) { + return [ + 'type' => 'hidden' + ]; + } + + $fields = [ + '_robotsHeadline' => [ + 'label' => 'seo.fields.robots.label', + 'type' => 'headline', + 'numbered' => false, + ] + ]; + + foreach ($kirby->option('tobimori.seo.robots.types') as $robots) { + $index = $kirby->option('tobimori.seo.robots.index'); + if (is_callable($index)) { + $index = $index(); + } + + $fields["robots{$robots}"] = [ + 'label' => "seo.fields.robots.{$robots}.label", + 'type' => 'toggles', + 'help' => "seo.fields.robots.{$robots}.help", + 'width' => '1/2', + 'default' => 'default', + 'reset' => false, + 'options' => [ + 'default' => t('seo.common.default') . ' ' . ($index ? t('seo.common.yes') : t('seo.common.no')), + 'true' => t('seo.common.yes'), + 'false' => t('seo.common.no'), + ] + ]; + } + + $fields['_seoLine3'] = [ + 'type' => 'line' + ]; + + return [ + 'type' => 'group', + 'fields' => $fields, + ]; +}; diff --git a/site/plugins/kirby-seo/blueprints/fields/social-media.php b/site/plugins/kirby-seo/blueprints/fields/social-media.php new file mode 100644 index 0000000..3bcf21e --- /dev/null +++ b/site/plugins/kirby-seo/blueprints/fields/social-media.php @@ -0,0 +1,30 @@ + $value) { + if ($value) { + $fields[$key] = [ + 'label' => ucfirst($key), + 'type' => 'url', + 'icon' => strtolower($key), + 'placeholder' => $value + ]; + } + } + + return [ + 'label' => 'seo.fields.socialMediaAccounts.label', + 'type' => 'object', + 'help' => 'seo.fields.socialMediaAccounts.help', + 'fields' => $fields + ]; +}; diff --git a/site/plugins/kirby-seo/blueprints/fields/title-template.yml b/site/plugins/kirby-seo/blueprints/fields/title-template.yml new file mode 100644 index 0000000..4b058ba --- /dev/null +++ b/site/plugins/kirby-seo/blueprints/fields/title-template.yml @@ -0,0 +1,6 @@ +type: seo-writer +nodes: + - seoTemplateTitle + - seoTemplateSiteTitle +toolbar: + inline: false diff --git a/site/plugins/kirby-seo/blueprints/page.php b/site/plugins/kirby-seo/blueprints/page.php new file mode 100644 index 0000000..1c7b5a4 --- /dev/null +++ b/site/plugins/kirby-seo/blueprints/page.php @@ -0,0 +1,60 @@ + 'seo.tabs.seo', + 'icon' => 'search', + 'columns' => [ + 'main' => [ + 'width' => '7/12', + 'fields' => [ + 'metaGroup' => 'seo/fields/meta-group', + 'ogGroup' => 'seo/fields/og-group', + 'robots' => 'seo/fields/robots', + 'metaInherit' => [ + 'label' => 'seo.fields.inheritSettings.label', + 'type' => 'multiselect', + 'help' => 'seo.fields.inheritSettings.help', + 'options' => [ + 'metaTemplate' => [ + '*' => 'seo.fields.metaTitleTemplate.label' + ], + 'metaDescription' => [ + '*' => 'seo.fields.metaDescription.label' + ], + 'ogTemplate' => [ + '*' => 'seo.fields.ogTitleTemplate.label' + ], + 'ogDescription' => [ + '*' => 'seo.fields.ogDescription.label' + ], + 'ogImage' => [ + '*' => 'seo.fields.ogImage.label' + ], + 'cropOgImage' => [ + '*' => 'seo.fields.cropOgImage.label' + ], + 'robots' => [ + '*' => 'seo.fields.robots.label' + ] + ] + ] + ] + ], + 'sidebar' => [ + 'width' => '5/12', + 'sticky' => true, + 'sections' => [ + 'seoPreview' => [ + 'type' => 'seo-preview' + ], + ...(Seo::option('searchConsole.enabled') ? [ + 'seoSearchConsole' => [ + 'type' => 'seo-search-console' + ] + ] : []) + ] + ] + ] +]; diff --git a/site/plugins/kirby-seo/blueprints/seo.php b/site/plugins/kirby-seo/blueprints/seo.php new file mode 100644 index 0000000..78f20b9 --- /dev/null +++ b/site/plugins/kirby-seo/blueprints/seo.php @@ -0,0 +1,15 @@ +request()->url()->toString(); + $isSite = Str::contains($path, '/site') && !Str::contains($path, '/pages/'); + + if ($isSite) { + return require __DIR__ . '/site.php'; + } + + return require __DIR__ . '/page.php'; +}; diff --git a/site/plugins/kirby-seo/blueprints/site.php b/site/plugins/kirby-seo/blueprints/site.php new file mode 100644 index 0000000..7a6d2ec --- /dev/null +++ b/site/plugins/kirby-seo/blueprints/site.php @@ -0,0 +1,95 @@ + 'seo.tabs.seo', + 'icon' => 'search', + 'columns' => [ + 'main' => [ + 'width' => '7/12', + 'fields' => [ + '_metaHeadline' => [ + 'label' => 'seo.site.meta.headline', + 'type' => 'headline', + 'help' => 'seo.site.meta.headline.help' + ], + 'metaTemplate' => [ + 'extends' => 'seo/fields/title-template', + 'label' => 'seo.fields.metaTitleTemplate.label', + 'help' => 'seo.fields.metaTitleTemplate.help' + ], + 'metaDescription' => [ + 'label' => 'seo.fields.metaDescription.label', + 'type' => 'seo-writer', + 'ai' => 'site-description', + 'help' => 'seo.fields.metaDescription.help' + ], + '_seoLine1' => [ + 'type' => 'line' + ], + '_ogHeadline' => [ + 'label' => 'seo.site.og.headline', + 'type' => 'headline', + 'numbered' => false, + 'help' => 'seo.site.og.headline.help' + ], + 'ogTemplate' => [ + 'extends' => 'seo/fields/title-template', + 'label' => 'seo.fields.ogTitleTemplate.label', + 'default' => '{{ title }}', + 'help' => 'seo.fields.metaTitleTemplate.help', + 'placeholder' => '{{ site.metaTemplate }}' + ], + 'ogDescription' => [ + 'label' => 'seo.fields.ogDescription.label', + 'type' => 'seo-writer', + 'ai' => 'og-site-description', + 'placeholder' => '{{ site.metaDescription }}' + ], + 'ogSiteName' => [ + 'label' => 'seo.fields.ogSiteName.label', + 'type' => 'text', + 'default' => '{{ site.title }}', + 'placeholder' => '{{ site.title }}', + 'width' => '1/2' + ], + 'ogImage' => [ + 'label' => 'seo.fields.ogImage.label', + 'extends' => 'seo/fields/og-image', + 'empty' => 'seo.fields.ogImage.empty', + 'width' => '1/2' + ], + 'cropOgImage' => [ + 'label' => 'seo.fields.cropOgImage.label', + 'type' => 'toggle', + 'default' => true, + 'text' => [ + "{{ t('seo.common.no') }}", + "{{ t('seo.common.yes') }}" + ], + 'help' => 'seo.fields.cropOgImage.help' + ], + '_seoLine2' => [ + 'type' => 'line' + ], + 'robots' => 'seo/fields/site-robots', + 'socialMediaAccounts' => 'seo/fields/social-media' + ] + ], + 'sidebar' => [ + 'width' => '5/12', + 'sticky' => true, + 'sections' => [ + 'seoPreview' => [ + 'type' => 'seo-preview' + ], + ...(Seo::option('searchConsole.enabled') ? [ + 'seoSearchConsole' => [ + 'type' => 'seo-search-console' + ] + ] : []) + ] + ] + ] +]; diff --git a/site/plugins/kirby-seo/classes/Ai.php b/site/plugins/kirby-seo/classes/Ai.php new file mode 100644 index 0000000..3cee361 --- /dev/null +++ b/site/plugins/kirby-seo/classes/Ai.php @@ -0,0 +1,62 @@ +stream($prompt, /* todo custom model here */); + } +} diff --git a/site/plugins/kirby-seo/classes/Ai/Chunk.php b/site/plugins/kirby-seo/classes/Ai/Chunk.php new file mode 100644 index 0000000..e36c076 --- /dev/null +++ b/site/plugins/kirby-seo/classes/Ai/Chunk.php @@ -0,0 +1,141 @@ + $message, + 'data' => $payload, + ]); + } + + public function isStreamStart(): bool + { + return $this->type === self::TYPE_STREAM_START; + } + + public function isStreamEnd(): bool + { + return $this->type === self::TYPE_STREAM_END; + } + + public function isTextStart(): bool + { + return $this->type === self::TYPE_TEXT_START; + } + + public function isTextDelta(): bool + { + return $this->type === self::TYPE_TEXT_DELTA; + } + + public function isTextComplete(): bool + { + return $this->type === self::TYPE_TEXT_COMPLETE; + } + + public function isThinkingStart(): bool + { + return $this->type === self::TYPE_THINKING_START; + } + + public function isThinkingDelta(): bool + { + return $this->type === self::TYPE_THINKING_DELTA; + } + + public function isThinkingComplete(): bool + { + return $this->type === self::TYPE_THINKING_COMPLETE; + } + + public function isToolCall(): bool + { + return $this->type === self::TYPE_TOOL_CALL; + } + + public function isToolResult(): bool + { + return $this->type === self::TYPE_TOOL_RESULT; + } + + public function isError(): bool + { + return $this->type === self::TYPE_ERROR; + } +} diff --git a/site/plugins/kirby-seo/classes/Ai/Driver.php b/site/plugins/kirby-seo/classes/Ai/Driver.php new file mode 100644 index 0000000..1817917 --- /dev/null +++ b/site/plugins/kirby-seo/classes/Ai/Driver.php @@ -0,0 +1,40 @@ + + */ + abstract public function stream(string $prompt, string|null $model = null): Generator; + + /** + * Returns a configuration value or throws when required. + */ + protected function config(string $key, mixed $default = null, bool $required = false): mixed + { + $value = Seo::option("ai.providers.{$this->providerId}.config.{$key}", $default); + + if ($required === true && ($value === null || $value === '')) { + throw new InvalidArgumentException( + "Missing required \"{$key}\" configuration for driver " . static::class . '.' + ); + } + + return $value; + } +} diff --git a/site/plugins/kirby-seo/classes/Ai/Drivers/Anthropic.php b/site/plugins/kirby-seo/classes/Ai/Drivers/Anthropic.php new file mode 100644 index 0000000..b6e3a83 --- /dev/null +++ b/site/plugins/kirby-seo/classes/Ai/Drivers/Anthropic.php @@ -0,0 +1,103 @@ +config('apiKey', required: true); + $headers = [ + 'Content-Type: application/json', + 'Accept: text/event-stream', + "x-api-key: {$apiKey}", + 'anthropic-version: 2023-06-01', + ]; + + $payload = [ + 'model' => $model ?? $this->config('model', static::DEFAULT_MODEL), + 'messages' => [ + ['role' => 'user', 'content' => $prompt] + ], + 'max_tokens' => 4096, + 'stream' => true, + ]; + + $stream = new SseStream($this->config('endpoint', static::DEFAULT_ENDPOINT), $headers, $payload, (int)$this->config('timeout', 120)); + yield from $stream->stream(function (array $event): Generator { + $type = $event['type'] ?? null; + + // handle message start event + if ($type === 'message_start') { + yield Chunk::streamStart($event); + return; + } + + // handle content block start (beginning of text output) + if ($type === 'content_block_start') { + $contentBlock = $event['content_block'] ?? []; + if (($contentBlock['type'] ?? null) === 'text') { + yield Chunk::textStart($event); + } + return; + } + + // handle content block delta (text chunks) + if ($type === 'content_block_delta') { + $delta = $event['delta'] ?? []; + if (($delta['type'] ?? null) === 'text_delta') { + $text = $delta['text'] ?? ''; + if ($text !== '') { + yield Chunk::textDelta($text, $event); + } + } + return; + } + + // handle content block stop (end of text block) + if ($type === 'content_block_stop') { + yield Chunk::textComplete($event); + return; + } + + // handle message stop (end of stream) + if ($type === 'message_stop') { + yield Chunk::streamEnd($event); + return; + } + + // handle ping events (keep-alive) + if ($type === 'ping') { + // ignore ping events + return; + } + + // handle error events + if ($type === 'error') { + $error = $event['error'] ?? []; + $message = $error['message'] ?? 'Unknown Anthropic streaming error.'; + yield Chunk::error($message, $event); + return; + } + + // handle message delta (contains usage info) + if ($type === 'message_delta') { + // we could extract usage info here if needed + return; + } + }); + } +} diff --git a/site/plugins/kirby-seo/classes/Ai/Drivers/OpenAi.php b/site/plugins/kirby-seo/classes/Ai/Drivers/OpenAi.php new file mode 100644 index 0000000..80dbb6d --- /dev/null +++ b/site/plugins/kirby-seo/classes/Ai/Drivers/OpenAi.php @@ -0,0 +1,82 @@ +config('apiKey', required: true); + $headers = [ + 'Content-Type: application/json', + 'Accept: text/event-stream', + "Authorization: Bearer {$apiKey}", + ]; + if ($organization = $this->config('organization')) { + $headers[] = "OpenAI-Organization: {$organization}"; + } + + $payload = [ + 'model' => $model ?? $this->config('model', static::DEFAULT_MODEL), + 'input' => $prompt, + // instructions does not work for e.g. openrouter so let's just put everything in user prompt + 'stream' => true, + ]; + + $stream = new SseStream($this->config('endpoint', static::DEFAULT_ENDPOINT), $headers, $payload, (int)$this->config('timeout', 120)); + yield from $stream->stream(function (array $event): Generator { + $type = $event['type'] ?? null; + + if ($type === 'response.created') { + yield Chunk::streamStart($event); + return; + } + + if ($type === 'response.in_progress') { + yield Chunk::textStart($event); + return; + } + + if ($type === 'response.output_text.delta') { + $delta = $event['delta'] ?? ''; + if ($delta !== '') { + yield Chunk::textDelta($delta, $event); + } + return; + } + + if ($type === 'response.output_text.done') { + yield Chunk::textComplete($event); + return; + } + + if ($type === 'response.completed') { + yield Chunk::streamEnd($event); + return; + } + + if ($type === 'response.output_item.added' && ($event['item']['type'] ?? null) === 'reasoning') { + yield Chunk::thinkingStart($event); + return; + } + + if ($type === 'response.error') { + $message = $event['error']['message'] ?? 'Unknown OpenAI streaming error.'; + yield Chunk::error($message, $event); + } + }); + } +} diff --git a/site/plugins/kirby-seo/classes/Ai/SseStream.php b/site/plugins/kirby-seo/classes/Ai/SseStream.php new file mode 100644 index 0000000..7f43464 --- /dev/null +++ b/site/plugins/kirby-seo/classes/Ai/SseStream.php @@ -0,0 +1,196 @@ + $mapper + * @return Generator + */ + public function stream(callable $mapper): Generator + { + $buffer = ''; + $response = ''; + $handle = curl_init($this->endpoint); + + curl_setopt_array($handle, [ + CURLOPT_POST => true, + CURLOPT_HTTPHEADER => $this->headers, + CURLOPT_POSTFIELDS => json_encode( + $this->payload, + JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES + ), + CURLOPT_RETURNTRANSFER => false, + CURLOPT_TIMEOUT => $this->timeout, + CURLOPT_WRITEFUNCTION => static function ($curl, $data) use (&$buffer, &$response) { + $buffer .= $data; + $currentLength = strlen($response); + + if ($currentLength < self::ERROR_CONTEXT_LIMIT) { + $response .= substr($data, 0, self::ERROR_CONTEXT_LIMIT - $currentLength); + } + + return strlen($data); + }, + ]); + + $multi = curl_multi_init(); + curl_multi_add_handle($multi, $handle); + + try { + $running = null; + do { + $status = curl_multi_exec($multi, $running); + + if ($status === CURLM_CALL_MULTI_PERFORM) { + continue; + } + + yield from $this->drainBuffer($buffer, $mapper); + + if ($running) { + curl_multi_select($multi, 0.1); + } + } while ($running); + + yield from $this->drainBuffer($buffer, $mapper, true); + + $errno = curl_errno($handle); + if ($errno) { + throw new KirbyException(curl_error($handle) ?: 'Streaming request failed.', $errno); + } + + $code = curl_getinfo($handle, CURLINFO_HTTP_CODE); + if ($code !== null && $code >= 400) { + $message = sprintf('Streaming request failed (%d)', $code); + $body = trim($response); + + if ($body !== '') { + $decoded = json_decode($body, true); + + if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) { + $body = $decoded['error']['message'] ?? $decoded['message'] ?? $body; + } + + if (strlen($body) > 200) { + $body = substr($body, 0, 200) . '...'; + } + + $message .= ': ' . preg_replace('/\s+/', ' ', $body); + } + + throw new KirbyException($message); + } + } finally { + curl_multi_remove_handle($multi, $handle); + curl_multi_close($multi); + curl_close($handle); + } + } + + /** + * @param callable(array $event): Generator $mapper + * @return Generator + */ + private function drainBuffer(string &$buffer, callable $mapper, bool $final = false): Generator + { + while ( + preg_match('/\r?\n\r?\n/', $buffer, $match, PREG_OFFSET_CAPTURE) === 1 + ) { + $pos = $match[0][1]; + $len = strlen($match[0][0]); + $frame = substr($buffer, 0, $pos); + $buffer = substr($buffer, $pos + $len); + + foreach ($this->mapFrame($frame, $mapper) as $chunk) { + yield $chunk; + } + } + + if ($final && trim($buffer) !== '') { + foreach ($this->mapFrame($buffer, $mapper) as $chunk) { + yield $chunk; + } + + $buffer = ''; + } + } + + /** + * @param callable(array $event): Generator $mapper + * @return Generator + */ + private function mapFrame(string $frame, callable $mapper): Generator + { + $frame = trim($frame); + + if ($frame === '') { + return; + } + + $payload = ''; + + foreach (preg_split("/\r\n|\n|\r/", $frame) as $line) { + $line = trim($line); + + if ($line === '' || str_starts_with($line, ':')) { + continue; + } + + if (str_starts_with($line, 'data:')) { + $payload .= substr($line, 5); + } + } + + $payload = trim($payload); + if ($payload === '' || $payload === '[DONE]') { + return; + } + + $event = json_decode($payload, true); + if (json_last_error() !== JSON_ERROR_NONE || !is_array($event)) { + return; + } + + yield from $mapper($event); + } +} diff --git a/site/plugins/kirby-seo/classes/Buttons/RobotsViewButton.php b/site/plugins/kirby-seo/classes/Buttons/RobotsViewButton.php new file mode 100644 index 0000000..468655b --- /dev/null +++ b/site/plugins/kirby-seo/classes/Buttons/RobotsViewButton.php @@ -0,0 +1,51 @@ +robots(); + + $theme = 'positive-icon'; + $icon = 'robots'; + $text = I18n::translate('seo.fields.robots.indicator.index'); + + if (Str::contains($robots, 'no') && !Str::contains($robots, 'noindex')) { + $theme = 'notice-icon'; + $icon = 'robots-off'; + $text = I18n::translate('seo.fields.robots.indicator.any'); + } + + if (Str::contains($robots, 'noindex')) { + $theme = 'negative-icon'; + $icon = 'robots-off'; + $text = I18n::translate('seo.fields.robots.indicator.noindex'); + } + + parent::__construct( + model: $model, + icon: $icon, + text: $text, + theme: $theme, + link: $model->panel()->url() . '?tab=seo', + responsive: true + ); + } +} diff --git a/site/plugins/kirby-seo/classes/Buttons/UtmShareViewButton.php b/site/plugins/kirby-seo/classes/Buttons/UtmShareViewButton.php new file mode 100644 index 0000000..014887b --- /dev/null +++ b/site/plugins/kirby-seo/classes/Buttons/UtmShareViewButton.php @@ -0,0 +1,21 @@ +panel()->path()}", + icon: 'share', + title: I18n::translate('seo.utmShare.button') + ); + } +} diff --git a/site/plugins/kirby-seo/classes/Dialogs/UtmShareDialog.php b/site/plugins/kirby-seo/classes/Dialogs/UtmShareDialog.php new file mode 100644 index 0000000..d2db98b --- /dev/null +++ b/site/plugins/kirby-seo/classes/Dialogs/UtmShareDialog.php @@ -0,0 +1,39 @@ +model = $kirby->site(); + } else { + $id = preg_replace('/^pages\//', '', $path); + $this->model = Find::page($id); + } + } + + public function load(): array + { + $url = $this->model instanceof Site + ? $this->model->homePage()->url() + : $this->model->url(); + + return [ + 'component' => 'k-seo-utm-share-dialog', + 'props' => [ + 'pageUrl' => $url + ] + ]; + } +} diff --git a/site/plugins/kirby-seo/classes/GoogleSearchConsole.php b/site/plugins/kirby-seo/classes/GoogleSearchConsole.php new file mode 100644 index 0000000..a71b431 --- /dev/null +++ b/site/plugins/kirby-seo/classes/GoogleSearchConsole.php @@ -0,0 +1,386 @@ +cache('tobimori.seo.searchConsole'); + } + + /** + * Get OAuth credentials from config + */ + public static function credentials(): ?array + { + return Seo::option('searchConsole.credentials.web'); + } + + /** + * Check if credentials are configured + */ + public static function hasCredentials(): bool + { + $credentials = static::credentials(); + return !empty($credentials['client_id']) && !empty($credentials['client_secret']); + } + + /** + * Get the token file path + */ + protected static function tokenPath(): string + { + $path = Seo::option('searchConsole.tokenPath'); + return is_callable($path) ? $path() : $path; + } + + /** + * Load tokens from file + */ + public static function tokens(): ?array + { + if (static::$tokens !== null) { + return static::$tokens; + } + + $path = static::tokenPath(); + if (!F::exists($path)) { + return null; + } + + static::$tokens = Json::read($path); + return static::$tokens; + } + + /** + * Save tokens to file + */ + protected static function saveTokens(array $tokens): void + { + static::$tokens = $tokens; + Json::write(static::tokenPath(), $tokens); + } + + /** + * Check if we have valid tokens + */ + public static function isConnected(): bool + { + $tokens = static::tokens(); + return !empty($tokens['access_token']) && !empty($tokens['refresh_token']); + } + + /** + * Get the authorization URL + */ + public static function authUrl(string $redirectUri, string $state): string + { + $credentials = static::credentials(); + + $uri = new Uri(static::AUTH_URL); + $uri->query()->merge([ + 'client_id' => $credentials['client_id'], + 'redirect_uri' => $redirectUri, + 'response_type' => 'code', + 'access_type' => 'offline', + 'prompt' => 'consent', + 'scope' => static::SCOPES, + 'state' => $state + ]); + + return $uri->toString(); + } + + /** + * Exchange authorization code for tokens + */ + public static function exchangeCode(string $code, string $redirectUri): array + { + $credentials = static::credentials(); + + $response = Remote::request(static::TOKEN_URL, [ + 'method' => 'POST', + 'headers' => [ + 'Content-Type' => 'application/x-www-form-urlencoded' + ], + 'data' => [ + 'client_id' => $credentials['client_id'], + 'client_secret' => $credentials['client_secret'], + 'code' => $code, + 'grant_type' => 'authorization_code', + 'redirect_uri' => $redirectUri + ] + ]); + + $data = $response->json(); + + if (isset($data['error'])) { + throw new \Exception($data['error_description'] ?? $data['error']); + } + + // store tokens with expiry timestamp + $tokens = [ + 'access_token' => $data['access_token'], + 'refresh_token' => $data['refresh_token'], + 'expires_at' => time() + $data['expires_in'] + ]; + + static::saveTokens($tokens); + return $tokens; + } + + /** + * Refresh the access token + */ + public static function refreshToken(): string + { + $credentials = static::credentials(); + $tokens = static::tokens(); + + if (empty($tokens['refresh_token'])) { + throw new \Exception('No refresh token available'); + } + + $response = Remote::request(static::TOKEN_URL, [ + 'method' => 'POST', + 'headers' => [ + 'Content-Type' => 'application/x-www-form-urlencoded' + ], + 'data' => [ + 'client_id' => $credentials['client_id'], + 'client_secret' => $credentials['client_secret'], + 'refresh_token' => $tokens['refresh_token'], + 'grant_type' => 'refresh_token' + ] + ]); + + $data = $response->json(); + + if (isset($data['error'])) { + throw new \Exception($data['error_description'] ?? $data['error']); + } + + $tokens['access_token'] = $data['access_token']; + $tokens['expires_at'] = time() + $data['expires_in']; + + static::saveTokens($tokens); + return $tokens['access_token']; + } + + /** + * Get a valid access token, refreshing if needed + */ + public static function accessToken(): string + { + $tokens = static::tokens(); + + if (empty($tokens['access_token'])) { + throw new \Exception('Not connected to Google Search Console'); + } + + // refresh if expired or expiring soon (within 5 min) + if ($tokens['expires_at'] < time() + 300) { + return static::refreshToken(); + } + + return $tokens['access_token']; + } + + /** + * Get the connected property URL + */ + public static function property(): ?string + { + $tokens = static::tokens(); + return $tokens['property'] ?? null; + } + + /** + * Find the best matching property for a site URL + */ + public static function findMatchingProperty(string $siteUrl): ?string + { + $properties = static::listProperties(); + if (empty($properties)) { + return null; + } + + $siteHost = parse_url($siteUrl, PHP_URL_HOST); + + foreach ($properties as $p) { + $propUrl = $p['siteUrl']; + + // check domain properties (sc-domain:example.com) + if (str_starts_with($propUrl, 'sc-domain:')) { + $domain = substr($propUrl, 10); + if ($domain === $siteHost || str_ends_with($siteHost, ".{$domain}")) { + return $propUrl; + } + } + + // check URL prefix properties + if (str_starts_with($siteUrl, $propUrl) || $propUrl === "{$siteUrl}/") { + return $propUrl; + } + } + + // fallback to first property + return $properties[0]['siteUrl'] ?? null; + } + + /** + * Set the connected property URL + */ + public static function setProperty(string $property): void + { + $tokens = static::tokens() ?? []; + $tokens['property'] = $property; + static::saveTokens($tokens); + } + + /** + * Disconnect (remove tokens) + */ + public static function disconnect(): void + { + $path = static::tokenPath(); + if (F::exists($path)) { + F::remove($path); + } + static::$tokens = null; + } + + /** + * List available GSC properties + */ + public static function listProperties(): array + { + $response = Remote::request('https://www.googleapis.com/webmasters/v3/sites', [ + 'method' => 'GET', + 'headers' => [ + 'Authorization' => 'Bearer ' . static::accessToken() + ] + ]); + + $data = $response->json(); + + if (isset($data['error'])) { + throw new \Exception($data['error']['message'] ?? 'Failed to list properties'); + } + + return $data['siteEntry'] ?? []; + } + + /** + * Query search analytics data (fetches max 25k rows from Google, cached for 24h) + */ + public static function query(array $options = []): array + { + $property = static::property(); + if (!$property) { + throw new \Exception('No property selected'); + } + + $body = [ + 'startDate' => $options['startDate'] ?? date('Y-m-d', strtotime('-28 days')), + 'endDate' => $options['endDate'] ?? date('Y-m-d', strtotime('-1 day')), + 'dimensions' => $options['dimensions'] ?? ['query'], + 'rowLimit' => 25000 + ]; + + if (!empty($options['page'])) { + $body['dimensionFilterGroups'] = [[ + 'filters' => [[ + 'dimension' => 'page', + 'operator' => $options['pageOperator'] ?? 'equals', + 'expression' => $options['page'] + ]] + ]]; + } + + $cacheKey = md5($property . json_encode($body)); + + $cached = static::cache()->get($cacheKey); + if ($cached !== null) { + return $cached; + } + + $uri = new Uri('https://www.googleapis.com/webmasters/v3/sites'); + $uri->setPath($uri->path() . '/' . urlencode($property) . '/searchAnalytics/query'); + + $response = Remote::request($uri->toString(), [ + 'method' => 'POST', + 'headers' => [ + 'Authorization' => 'Bearer ' . static::accessToken(), + 'Content-Type' => 'application/json' + ], + 'data' => json_encode($body) + ]); + + $data = $response->json(); + + if (isset($data['error'])) { + throw new \Exception($data['error']['message'] ?? 'Failed to query search analytics'); + } + + $rows = $data['rows'] ?? []; + + static::cache()->set($cacheKey, $rows, static::CACHE_DURATION); + + return $rows; + } + + /** + * Query search data for a Kirby model (page or site), sorted by metric + */ + public static function queryForModel($model, string $metric = 'clicks', int $limit = 10, bool $asc = false): array + { + if ($model instanceof Page) { + // try exact URL match first + $data = static::query(['page' => $model->url()]); + + // fallback: match by path + if (empty($data)) { + $path = $model->uri(); + if ($path) { + $data = static::query([ + 'page' => "/{$path}", + 'pageOperator' => 'contains' + ]); + } + } + } else { + $data = static::query(); + } + + if (empty($data)) { + return []; + } + + $dir = $asc ? 1 : -1; + usort($data, fn ($a, $b) => match ($metric) { + 'query' => strcasecmp($a['keys'][0], $b['keys'][0]) * $dir, + default => ($a[$metric] <=> $b[$metric]) * $dir + }); + + return array_slice($data, 0, $limit); + } +} diff --git a/site/plugins/kirby-seo/classes/IndexNow.php b/site/plugins/kirby-seo/classes/IndexNow.php new file mode 100644 index 0000000..5d16711 --- /dev/null +++ b/site/plugins/kirby-seo/classes/IndexNow.php @@ -0,0 +1,316 @@ +page = $page; + + // always add the current page if it's indexable + if ($this->isIndexable($page)) { + $this->urls[] = $page->url(); + } + } + + /** + * Collect URLs to be indexed based on rules + */ + public function collect(): self + { + if ($this->collected) { + return $this; + } + + $rules = Seo::option('indexnow.rules') ?? []; + + foreach ($rules as $pattern => $invalidations) { + if (!$this->matchesPattern($pattern)) { + continue; + } + + $this->collectFromRule($invalidations); + } + + $this->urls = array_unique($this->urls); + $this->collected = true; + + return $this; + } + + /** + * Get collected urls + */ + public function urls(): array + { + if (!$this->collected) { + $this->collect(); + } + + return $this->urls; + } + + /** + * Send the collected urls + */ + public function request(): bool + { + if (!$this->collected) { + $this->collect(); + } + + return static::send($this->urls); + } + + /** + * Static method to send urls to indexnow api + */ + public static function send(array $urls): bool + { + if (!Seo::option('indexnow.enabled') || empty($urls)) { + return false; + } + + $firstUrl = $urls[0]; + $parsedUrl = parse_url($firstUrl); + $host = $parsedUrl['host']; + $scheme = $parsedUrl['scheme'] ?? 'https'; + $path = $parsedUrl['path'] ?? ''; + + // don't send requests for local development environments + if (App::instance()->environment()->isLocal()) { + return false; + } + + // get base path (everything before the page path) + $basePath = ''; + if ($path && $path !== '/') { + // find the base path by comparing with site url + $siteUrl = App::instance()->site()->url(); + $siteParsed = parse_url($siteUrl); + $basePath = $siteParsed['path'] ?? ''; + } + + $searchEngine = Seo::option('indexnow.searchEngine'); + $searchEngine = rtrim($searchEngine, '/'); + if (!str_contains($searchEngine, '/indexnow')) { + $searchEngine .= '/indexnow'; + } + + $domainUrls = array_filter($urls, fn ($url) => parse_url($url, PHP_URL_HOST) === $host); + + // split into batches of 10,000 (IndexNow limit) + $batches = array_chunk(array_values(array_unique($domainUrls)), 10000); + $allSuccessful = true; + $key = static::key(); + + foreach ($batches as $batch) { + try { + $response = Remote::post($searchEngine, [ + 'headers' => [ + 'Content-Type' => 'application/json; charset=utf-8', + 'User-Agent' => Seo::userAgent() + ], + 'data' => json_encode([ + 'host' => $host, + 'key' => $key, + 'keyLocation' => "{$scheme}://{$host}{$basePath}/indexnow-{$key}.txt", + 'urlList' => $batch + ]) + ]); + + if ($response->code() > 299) { + $allSuccessful = false; + } + } catch (\Exception $e) { + $allSuccessful = false; + } + } + + return $allSuccessful; + } + + /** + * Get or generate the indexnow key + * Stored in cache so it persists across requests + */ + public static function key(): string + { + return static::$key ??= App::instance()->cache('tobimori.seo.indexnow')->getOrSet('key', fn () => Str::random(32, 'hexLower'), 0); + } + + /** + * Check if a provided key matches the stored key + * Used by the route to verify ownership + */ + public static function verifyKey(string $providedKey): bool + { + return $providedKey === static::key(); + } + + /** + * Check if page matches a pattern (url glob/regex or template) + */ + protected function matchesPattern(string $pattern): bool + { + if ($pattern === '*') { + return true; + } + + // url pattern + if (str_contains($pattern, '/')) { + return $this->matchesUrlPattern($pattern, $this->page->url()); + } + + // template pattern + return $this->page->intendedTemplate()->name() === $pattern; + } + + /** + * Match url pattern (glob style) + */ + protected function matchesUrlPattern(string $pattern, string $url): bool + { + // convert glob to regex + $pattern = str_replace( + ['*', '?', '[', ']'], + ['.*', '.', '\[', '\]'], + $pattern + ); + + return preg_match("#^{$pattern}$#", parse_url($url, PHP_URL_PATH)); + } + + /** + * Collect urls based on invalidation rules + */ + protected function collectFromRule(array $rule): void + { + // parent(s) + if (isset($rule['parent'])) { + $this->collectParents($rule['parent']); + } + + // children + if (isset($rule['children'])) { + $this->collectChildren($rule['children']); + } + + // siblings + if (isset($rule['siblings']) && $rule['siblings'] === true) { + $this->collectSiblings(); + } + + // specific urls + if (isset($rule['urls'])) { + foreach ($rule['urls'] as $url) { + $this->urls[] = url($url); + } + } + + // pages by template + if (isset($rule['templates'])) { + $this->collectByTemplates($rule['templates']); + } + } + + /** + * Collect parent urls + */ + protected function collectParents($levels): void + { + $parent = $this->page->parent(); + $count = is_bool($levels) ? 1 : $levels; + $language = App::instance()->language(); + + while ($parent && $count > 0) { + if ($this->isIndexable($parent)) { + $this->urls[] = $parent->url($language?->code()); + } + $parent = $parent->parent(); + $count--; + } + } + + /** + * Collect children urls + */ + protected function collectChildren($depth): void + { + $maxDepth = is_bool($depth) ? null : $depth; + $language = App::instance()->language(); + + $collectRecursive = function ($page, $currentDepth = 0) use (&$collectRecursive, $maxDepth, $language) { + if ($maxDepth !== null && $currentDepth >= $maxDepth) { + return; + } + + foreach ($page->children() as $child) { + if ($this->isIndexable($child)) { + $this->urls[] = $child->url($language?->code()); + } + $collectRecursive($child, $currentDepth + 1); + } + }; + + $collectRecursive($this->page); + } + + /** + * Collect sibling urls + */ + protected function collectSiblings(): void + { + if (!$this->page->parent()) { + return; + } + + $language = App::instance()->language(); + + foreach ($this->page->siblings() as $sibling) { + if ($this->isIndexable($sibling)) { + $this->urls[] = $sibling->url($language?->code()); + } + } + } + + /** + * Collect urls by template names + */ + protected function collectByTemplates(array $templates): void + { + $language = App::instance()->language(); + + $pages = $this->page->site()->index() + ->filterBy('intendedTemplate', 'in', $templates) + ->filter($this->isIndexable(...)); + + foreach ($pages as $page) { + $this->urls[] = $page->url($language?->code()); + } + } + + /** + * Check if a page is indexable (robots allow + listed) + */ + protected function isIndexable(Page $page): bool + { + return $page->isListed() + && $page->robots() !== 'noindex' + && $page->robots() !== 'none'; + } +} diff --git a/site/plugins/kirby-seo/classes/Meta.php b/site/plugins/kirby-seo/classes/Meta.php new file mode 100644 index 0000000..ac12f67 --- /dev/null +++ b/site/plugins/kirby-seo/classes/Meta.php @@ -0,0 +1,743 @@ +page = $page; + $this->lang = $lang ?? kirby()->language(); + + if (method_exists($this->page, 'metaDefaults')) { + $this->metaDefaults = $this->page->metaDefaults($this->lang?->code()); + } + } + + /** + * Normalize a locale string to use a specific separator + * + * @param string $locale The locale string (e.g., 'en_US.UTF-8', 'en-US', 'en_US') + * @param string $separator The separator to use ('-' for BCP47/hreflang, '_' for Open Graph) + * @return string The normalized locale (e.g., 'en-US' or 'en_US') + */ + public static function normalizeLocale(string $locale, string $separator = '-'): string + { + // encoding suffix if present (e.g., '.UTF-8') + $locale = Str::contains($locale, '.') ? Str::before($locale, '.') : $locale; + + // target both underscores and hyphens + $locale = Str::replace($locale, '_', $separator); + $locale = Str::replace($locale, '-', $separator); + + return $locale; + } + + /** + * Convert a Language to BCP 47 language tag format for hreflang attributes + * + * @param Language $language + * @return string The BCP 47 compliant language tag (e.g., 'en-US', 'de-DE') + */ + public static function toBCP47(Language $language): string + { + return self::normalizeLocale($language->locale(LC_ALL), '-'); + } + + /** + * Convert a Language to Open Graph locale format + * + * @param Language $language + * @return string The Open Graph locale format (e.g., 'en_US', 'de_DE') + */ + public static function toOpenGraphLocale(Language $language): string + { + return self::normalizeLocale($language->locale(LC_ALL), '_'); + } + + /** + * Returns the meta array which maps meta tags to their fieldnames + */ + protected function metaArray(): array + { + if ($this->metaArray) { + return $this->metaArray; + } + + // We have to specify field names and resolve them later, so we can use this + // function to resolve meta tags from field names in the programmatic function + $meta = + [ + 'title' => 'metaTitle', + 'description' => 'metaDescription', + 'date' => fn () => $this->page->modified($this->dateFormat()), + 'og:title' => 'ogTitle', + 'og:description' => 'ogDescription', + 'og:site_name' => 'ogSiteName', + 'og:image' => 'ogImage', + 'og:image:width' => fn () => $this->ogImageThumb()?->width() ?? null, + 'og:image:height' => fn () => $this->ogImageThumb()?->height() ?? null, + 'og:image:alt' => fn () => $this->get('ogImage')->toFile()?->alt() ?? null, + 'og:type' => 'ogType', + ]; + + + // Robots + if ($robotsActive = Seo::option('robots.active')) { + $meta['robots'] = $this->robots(...); + } + + // only add canonical and alternate tags if the page is indexable + // we have to resolve this lazily (using a callable) to avoid an infinite loop + $allowsIndexFn = fn () => !$robotsActive || !Str::contains($this->robots(), 'noindex'); + + // canonical + $canonicalFn = fn () => $allowsIndexFn() ? $this->canonicalUrl() : null; + $meta['canonical'] = $canonicalFn; + $meta['og:url'] = $canonicalFn; + + // Check if the current URL is canonical + // Compare the current request URL with the canonical URL + $currentUrl = kirby()->request()->url()->toString(); + $canonicalUrl = $this->canonicalUrl(); + $isCanonical = $currentUrl === $canonicalUrl; + + // Multi-lang alternate tags + // Skip hreflang tags if URL is not canonical (has query params, Kirby params, etc.) + if (kirby()->languages()->count() > 1 && $this->lang !== null && $isCanonical) { + foreach (kirby()->languages() as $lang) { + // only if this language is translated for this page and exists + // note: can be checked now, does not cause infinite loop + if (!$this->page->translation($lang->code())->exists()) { + continue; + } + + // only add alternate tags if the page is indexable + $meta['alternate'][] = fn () => $allowsIndexFn() ? [ + 'hreflang' => Meta::toBCP47($lang), + 'href' => $this->page->url($lang->code()), + 'rel' => 'alternate', + ] : null; + + if ($lang !== $this->lang) { + $meta['og:locale:alternate'][] = fn () => Meta::toOpenGraphLocale($lang); + } + } + + // x-default: language-neutral URL for users whose language doesn't match any translation + // indexUrl() strips the language prefix so the server can handle language detection + // see config/page-methods.php for details and customization + $meta['alternate'][] = fn () => $allowsIndexFn() ? [ + 'hreflang' => 'x-default', + 'href' => $this->page->indexUrl(), + 'rel' => 'alternate', + ] : null; + $meta['og:locale'] = fn () => Meta::toOpenGraphLocale($this->lang); + } else { + // Single-language site: get locale from cascade (will fallback to 'locale' option) + $meta['og:locale'] = fn () => Meta::normalizeLocale($this->get('locale')->value(), '_'); + } + + // If URL is not canonical, also skip og:locale:alternate tags + if (!$isCanonical) { + unset($meta['og:locale:alternate']); + } + + $meta['me'] = fn () => ( + ($socialMedia = $this->site('socialMediaAccounts')?->toObject()) + && ($mastodon = $socialMedia->mastodon()->value()) + ) ? $mastodon : null; + + // This array will be normalized for use in the snippet in $this->snippetData() + return $this->metaArray = $meta; + } + + /** + * This array defines what HTML tag the corresponding meta tags are using including the attributes, + * so everything is a bit more elegant when defining programmatic content (supports regex) + */ + public const TAG_TYPE_MAP = [ + [ + 'tag' => 'title', + 'priority' => true, + 'tags' => [ + 'title' + ] + ], + [ + 'tag' => 'link', + 'attributes' => [ + 'name' => 'rel', + 'content' => 'href', + ], + 'tags' => [ + 'me', + 'canonical', + 'alternate', + ] + ], + [ + 'tag' => 'meta', + 'attributes' => [ + 'name' => 'property', + 'content' => 'content', + ], + 'tags' => [ + '/og:.+/' + ] + ] + ]; + + /** + * Normalize the meta array and remaining meta defaults to be used in the snippet, + * also resolve the content, if necessary + */ + public function snippetData(?array $raw = null): array + { + $mergeWithDefaults = !isset($raw); + $raw ??= $this->metaArray(); + $tags = []; + + foreach ($raw as $name => $value) { + // if the key is numeric, it is already normalized to the correct array syntax + if (is_numeric($name)) { + // but we still check if the array is valid + if (!is_array($value) || count(array_intersect(['tag', 'content', 'attributes'], array_keys($value))) !== count($value)) { + throw new InvalidArgumentException("[Kirby SEO] Invalid array structure found in programmatic content for page {$this->slug()}. Please check your metaDefaults method for template {$this->template()->name()}."); + } + + $tags[] = $value; + continue; + } + + // allow overrides from metaDefaults for keys that are a callable or array by default + // (all fields from meta array that are not part of the regular cascade) + if ((is_callable($value) || is_array($value)) && $mergeWithDefaults && array_key_exists($name, $this->metaDefaults)) { + $this->consumed[] = $name; + $value = $this->metaDefaults[$name]; + } + + // if the value is a string, we know it's a field name + if (is_string($value)) { + $value = $this->$value($name); + } + + // if the value is a callable, we resolve it + if (is_callable($value)) { + $value = $value($this->page); + } + + // if the value is empty, we don't want to output it + if ((is_a($value, Field::class) && $value->isEmpty()) || !$value) { + continue; + } + + // resolve the tag type from the meta array + // so we can use the correct attributes to normalize it + $tag = $this->resolveTag($name); + + // if the value is an associative array now, all of them are attributes + // and we don't look for what the TAG_TYPE_MAP says + // or there should be multiple tags with the same name (non-associative array) + if (is_array($value)) { + if (!A::isAssociative($value)) { + foreach ($value as $val) { + $tags = array_merge($tags, $this->snippetData([$name => $val])); + } + continue; + } + + // array is associative, so it's an array of attributes + // we resolve the values, if they are callable + array_walk($value, function (&$item) { + if (is_callable($item)) { + $item = $item($this->page); + } + }); + + // add the tag to the array + $tags[] = [ + 'tag' => $tag['tag'], + 'attributes' => $value, + 'content' => null, + 'priority' => $tag['priority'] ?? false, + ]; + continue; + } + + // if the value is a string, we use the TAG_TYPE_MAP + // to correctly map the attributes + $tags[] = [ + 'tag' => $tag['tag'], + 'attributes' => isset($tag['attributes']) ? [ + $tag['attributes']['name'] => $name, + $tag['attributes']['content'] => $value, + ] : null, + 'content' => !isset($tag['attributes']) ? $value : null, + 'priority' => $tag['priority'] ?? false, + ]; + } + + if ($mergeWithDefaults) { + // merge the remaining meta defaults + $tags = array_merge($tags, $this->snippetData(array_diff_key($this->metaDefaults, array_flip($this->consumed)))); + } + + return $tags; + } + + /** + * Resolves the tag type from the meta array + */ + protected function resolveTag(string $tag): array + { + foreach (self::TAG_TYPE_MAP as $type) { + foreach ($type['tags'] as $regexOrString) { + // Check if the supplied tag is a regex or a normal tag name + if (Str::startsWith($regexOrString, '/') && Str::endsWith($regexOrString, '/') ? + Str::match($tag, $regexOrString) : $tag === $regexOrString + ) { + return $type; + } + } + } + + return [ + 'tag' => 'meta', + 'attributes' => [ + 'name' => 'name', + 'content' => 'content', + ] + ]; + } + + /** + * Magic method to get a meta value by calling the method name + */ + public function __call($name, $args = null): mixed + { + if (method_exists($this, $name)) { + return $this->$name($args); + } + + return $this->get($name); + } + + /** + * Get the meta value for a given key + */ + public function get(string $key, array $exclude = []): Field + { + $cascade = Seo::option('cascade'); + if (count(array_intersect(get_class_methods($this), $cascade)) !== count($cascade)) { + throw new InvalidArgumentException('[Kirby SEO] Invalid cascade method in config. Please check your options for `tobimori.seo.cascade`.'); + } + + // Track consumed keys, so we don't output legacy field values + $toBeConsumed = $key; + if ( + (array_key_exists($toBeConsumed, $this->metaDefaults) + || array_key_exists($toBeConsumed = $this->findTagForField($toBeConsumed), $this->metaDefaults)) + && !in_array($toBeConsumed, $this->consumed) + ) { + $this->consumed[] = $toBeConsumed; + } + + foreach (array_diff($cascade, $exclude) as $method) { + if ($field = $this->$method($key)) { + if ( + is_string($value = $field->value()) + && Str::contains($value, 'data-seo-template-variable') + ) { + $value = Str::unhtml($value); + return new Field($this->page, $key, $value); + } + + return $field; + } + } + + return new Field($this->page, $key, ''); + } + + /** + * Get the meta value for a given key from the page's fields + */ + protected function fields(string $key): Field|null + { + if (($field = $this->page->content($this->lang?->code())->get($key))) { + if (Str::contains($key, 'robots') && !Seo::option('robots.pageSettings')) { + return null; + } + + if ($field->isNotEmpty() && !A::has(self::DEFAULT_VALUES, $field->value())) { + return $field; + } + } + + return null; + } + + /** + * Maps Open Graph fields to Meta fields for fallbackFields + * cascade method + */ + public const FALLBACK_MAP = [ + 'ogTitle' => 'metaTitle', + 'ogDescription' => 'metaDescription', + 'ogTemplate' => 'metaTemplate', + ]; + + /** + * We only allow the following cascade methods for fallbacks, + * because we don't want to fallback to the config defaults for + * Meta fields, because we most likely already have those set + * for the Open Graph fields + */ + public const FALLBACK_CASCADE = [ + 'fields', + 'programmatic', + 'parent', + 'site' + ]; + + /** + * Get the meta value for a given key using the fallback fields + * defined above (usually Open Graph > Meta Fields) + */ + protected function fallbackFields(string $key): Field|null + { + if (array_key_exists($key, self::FALLBACK_MAP)) { + $fallback = self::FALLBACK_MAP[$key]; + $cascade = Seo::option('cascade'); + + foreach (array_intersect($cascade, self::FALLBACK_CASCADE) as $method) { + if ($field = $this->$method($fallback)) { + return $field; + } + } + } + + return null; + } + + protected function findTagForField(string $fieldName): string|null + { + return array_search($fieldName, $this->metaArray()); + } + + /** + * Get the meta value for a given key from the page's meta + * array, which can be set in the page's model metaDefaults method + */ + protected function programmatic(string $key): Field|null + { + if (!$this->metaDefaults) { + return null; + } + + // Check if the key (field name) is in the array syntax + if (array_key_exists($key, $this->metaDefaults)) { + $val = $this->metaDefaults[$key]; + } + + // If there is no programmatic value for the key, + // try looking it up in the meta array + // maybe it is a meta tag and not a field name? + if (!isset($val) && ($key = $this->findTagForField($key)) && array_key_exists($key, $this->metaDefaults)) { + $val = $this->metaDefaults[$key]; + } + + if (isset($val)) { + if (is_callable($val)) { + $val = $val($this->page); + } + + if (is_array($val)) { + $val = $val['content'] ?? $val['href'] ?? null; + + // Last sanity check, if the array syntax doesn't have a supported key + if ($val === null) { + // Remove the key from the consumed array, so it doesn't get filtered out + // (we can assume the entry is a custom meta tag that uses different attributes) + $this->consumed = array_filter($this->consumed, fn ($item) => $item !== $key); + return null; + } + } + + if (is_a($val, 'Kirby\Content\Field')) { + return new Field($this->page, $key, $val->value()); + } + + return new Field($this->page, $key, $val); + } + + return null; + } + + /** + * Get the meta value for a given key from the page's parent, + * if the page is allowed to inherit the value + */ + protected function parent(string $key): Field|null + { + if ($this->canInherit($key)) { + $parent = $this->page->parent(); + $parentMeta = new Meta($parent, $this->lang); + if ($value = $parentMeta->get($key)) { + return $value; + } + } + + return null; + } + + /** + * Get the meta value for a given key from the + * site's meta blueprint & content + */ + protected function site(string $key): Field|null + { + if (($site = $this->page->site()->content($this->lang?->code())->get($key)) && ($site->isNotEmpty() && !A::has(self::DEFAULT_VALUES, $site->value))) { + return $site; + } + + return null; + } + + /** + * Get the meta value for a given key from the + * config.php options + */ + protected function options(string $key): Field|null + { + if ($option = Seo::option("default.{$key}", args: [$this->page])) { + if (is_a($option, Field::class)) { + return $option; + } + + return new Field($this->page, $key, $option); + } + + return null; + } + + /** + * Checks if the page can inherit a meta value from its parent + */ + private function canInherit(string $key): bool + { + $parent = $this->page->parent(); + if (!$parent) { + return false; + } + + $inherit = $parent->metaInherit()->split(); + if (Str::contains($key, 'robots') && A::has($inherit, 'robots')) { + return true; + } + return A::has($inherit, $key); + } + + /** + * Applies the title template, and returns the correct title + */ + public function metaTitle() + { + $title = $this->get('metaTitle'); + $template = $this->get('metaTemplate'); + + $useTemplate = $this->page->useTitleTemplate(); + $useTemplate = $useTemplate->isEmpty() ? true : $useTemplate->toBool(); + + $string = $title->value(); + if ($useTemplate) { + $string = $this->page->toString( + $template, + ['title' => $title] + ); + } + + return new Field( + $this->page, + 'metaTitle', + $string + ); + } + + /** + * Applies the OG title template, and returns the OG Title + */ + public function ogTitle() + { + $title = $this->get('metaTitle'); + $template = $this->get('ogTemplate'); + + $useTemplate = $this->page->useOgTemplate(); + $useTemplate = $useTemplate->isEmpty() ? true : $useTemplate->toBool(); + + $string = $title->value(); + if ($useTemplate) { + $string = $this->page->toString( + $template, + ['title' => $title] + ); + } + + return new Field( + $this->page, + 'ogTitle', + $string + ); + } + + /** + * Gets the canonical url for the page + */ + public function canonicalUrl() + { + return $this->page->site()->canonicalFor($this->page->url()); + } + + /** + * Gets the date format for modified meta tags, based on the registered date handler + */ + public function dateFormat(): string + { + if ($custom = Seo::option('dateFormat')) { + return $custom; + } + + switch (option('date.handler')) { + case 'strftime': + return '%Y-%m-%d'; + case 'intl': + return 'yyyy-MM-dd'; + case 'date': + default: + return 'Y-m-d'; + } + } + + /** + * Get the pages' robots rules as string + */ + public function robots() + { + $robots = []; + foreach (Seo::option('robots.types') as $type) { + if (!$this->get('robots' . Str::ucfirst($type))->toBool()) { + $robots[] = 'no' . Str::lower($type); + } + } + + if (A::count($robots) === 0) { + $robots = ['all']; + } + + return A::join($robots, ','); + } + + /** + * Get the og:image thumb object + */ + public function ogImageThumb(): FileVersion|null + { + $field = $this->get('ogImage'); + + // Only process if we have a file object + if ($file = $field->toFile()) { + $cropOgImage = $this->get('cropOgImage')->toBool(); + + if ($cropOgImage) { + // Crop to 1200x630 + return $file->thumb([ + 'width' => 1200, + 'height' => 630, + 'crop' => true, + ]); + } else { + // Resize to max 1500px on the longest side + return $file->thumb([ + 'width' => 1500, + 'height' => 1500, + 'upscale' => false, + ]); + } + } + + // Return null if it's a custom URL or empty + return null; + } + + /** + * Get the og:image url + */ + public function ogImage(): string|null + { + if ($ogImage = $this->ogImageThumb()) { + return $ogImage->url(); + } + + $field = $this->get('ogImage'); + if ($field->isNotEmpty()) { + return $field->value(); + } + + return null; + } + + /** + * Helper method the get the current page from the URL path, + * for use in programmatic blueprints + */ + public static function currentPage(): Page|null + { + $path = App::instance()->request()->url()->toString(); + $matches = Str::match($path, "/pages\/([a-zA-Z0-9-_+]+)\/?/m"); + $segments = Str::split($matches[1], '+'); + + $page = App::instance()->site(); + foreach ($segments as $segment) { + if ($page = $page->findPageOrDraft($segment)) { + continue; + } + + return null; + } + + return $page; + } +} diff --git a/site/plugins/kirby-seo/classes/SchemaSingleton.php b/site/plugins/kirby-seo/classes/SchemaSingleton.php new file mode 100644 index 0000000..1e44c6a --- /dev/null +++ b/site/plugins/kirby-seo/classes/SchemaSingleton.php @@ -0,0 +1,37 @@ +id() ?? 'default'][$type])) { + self::$instances[$page?->id() ?? 'default'][$type] = Schema::{$type}(); + } + + return self::$instances[$page?->id() ?? 'default'][$type]; + } + + public static function getInstances(Page|null $page = null): array + { + if (!class_exists('Spatie\SchemaOrg\Schema')) { + return []; + } + + return self::$instances[$page?->id() ?? 'default'] ?? []; + } +} diff --git a/site/plugins/kirby-seo/classes/Seo.php b/site/plugins/kirby-seo/classes/Seo.php new file mode 100644 index 0000000..735189e --- /dev/null +++ b/site/plugins/kirby-seo/classes/Seo.php @@ -0,0 +1,29 @@ +version() . " (+https://plugins.andkindness.com/seo)"; + } + + /** + * Returns a plugin option + */ + public static function option(string $key, mixed $default = null, mixed $args = []): mixed + { + $option = App::instance()->option("tobimori.seo.{$key}", $default); + if (is_callable($option)) { + $option = $option(...$args); + } + + return $option; + } +} diff --git a/site/plugins/kirby-seo/classes/Sitemap/Sitemap.php b/site/plugins/kirby-seo/classes/Sitemap/Sitemap.php new file mode 100644 index 0000000..2d29c98 --- /dev/null +++ b/site/plugins/kirby-seo/classes/Sitemap/Sitemap.php @@ -0,0 +1,87 @@ +key; + } + + public function loc(): string + { + return kirby()->site()->canonicalFor('sitemap-' . $this->key . '.xml'); + } + + public function lastmod(): string + { + $lastmod = 0; + foreach ($this as $url) { + $lastmod = max($lastmod, strtotime($url->lastmod())); + } + + if ($lastmod > 0) { + return date('c', $lastmod); + } + + return date('c'); + } + + public function createUrl(string $loc): SitemapUrl + { + $url = $this->makeUrl($loc); + $this->append($url); + return $url; + } + + public static function makeUrl(string $url): SitemapUrl + { + return new SitemapUrl($url); + } + + public function toDOMNode(DOMDocument $doc = new DOMDocument('1.0', 'UTF-8')) + { + $doc->formatOutput = true; + + $root = $doc->createElement('sitemap'); + foreach (['loc', 'lastmod'] as $key) { + $root->appendChild($doc->createElement($key, $this->$key())); + } + + return $root; + } + + public function toString(): string + { + $doc = new DOMDocument('1.0', 'UTF-8'); + $doc->formatOutput = true; + + $stylesheetUrl = kirby()->site()->canonicalFor("sitemap.xsl", true); + $doc->appendChild($doc->createProcessingInstruction('xml-stylesheet', 'type="text/xsl" href="' . $stylesheetUrl . '"')); + + $root = $doc->createElementNS('http://www.sitemaps.org/schemas/sitemap/0.9', 'urlset'); + $root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xhtml', 'http://www.w3.org/1999/xhtml'); + + // version can be null when installing branches during development + if ($version = App::plugin('tobimori/seo')->version()) { + $root->setAttribute('seo-version', $version); + } + + foreach ($this as $url) { + $root->appendChild($url->toDOMNode($doc)); + } + + $doc->appendChild($root); + return $doc->saveXML(); + } +} diff --git a/site/plugins/kirby-seo/classes/Sitemap/SitemapIndex.php b/site/plugins/kirby-seo/classes/Sitemap/SitemapIndex.php new file mode 100644 index 0000000..7ced6a1 --- /dev/null +++ b/site/plugins/kirby-seo/classes/Sitemap/SitemapIndex.php @@ -0,0 +1,101 @@ +make($key); + $this->append($sitemap); + return $sitemap; + } + + public static function make(string $key = 'pages'): Sitemap + { + return new Sitemap($key); + } + + public static function makeUrl(string $url): SitemapUrl + { + return new SitemapUrl($url); + } + + public function toString(): string + { + $doc = new DOMDocument('1.0', 'UTF-8'); + $doc->formatOutput = true; + + $stylesheetUrl = kirby()->site()->canonicalFor("sitemap.xsl", true); + $doc->appendChild($doc->createProcessingInstruction('xml-stylesheet', 'type="text/xsl" href="' . $stylesheetUrl . '"')); + + $root = $doc->createElementNS('http://www.sitemaps.org/schemas/sitemap/0.9', 'sitemapindex'); + $root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xhtml', 'http://www.w3.org/1999/xhtml'); + $root->setAttribute('seo-version', App::plugin('tobimori/seo')->version()); + $doc->appendChild($root); + + foreach ($this as $sitemap) { + $root->appendChild($sitemap->toDOMNode($doc)); + } + + return $doc->saveXML(); + } + + public function isValidIndex(?string $key = null): bool + { + if ($key === null) { + return $this->count() > 1; + } + + return !!$this->findBy('key', $key) && $this->count() > 1; + } + + public function generate(): void + { + $generator = option('tobimori.seo.sitemap.generator'); + if (is_callable($generator)) { + $generator($this); + } + } + + public function render(Page $page): string|null + { + // There always has to be at least one index, + // otherwise the sitemap will fail to render + if ($this->count() === 0) { + $this->generate(); + } + + if ($this->count() === 0) { + $this->create(); + } + + if (($index = $page->content()->get('index'))->isEmpty()) { + // If there is only one index, we do not need to render the index page + return $this->count() > 1 ? $this->toString() : $this->first()->toString(); + } + + $sitemap = $this->findBy('key', $index->value()); + if ($sitemap) { + return $sitemap->toString(); + } + + return null; + } +} diff --git a/site/plugins/kirby-seo/classes/Sitemap/SitemapUrl.php b/site/plugins/kirby-seo/classes/Sitemap/SitemapUrl.php new file mode 100644 index 0000000..1ee372f --- /dev/null +++ b/site/plugins/kirby-seo/classes/Sitemap/SitemapUrl.php @@ -0,0 +1,115 @@ +loc; + } + + $this->loc = $url; + return $this; + } + + public function lastmod(?string $lastmod = null): SitemapUrl|string + { + if ($lastmod === null) { + return $this->lastmod; + } + + $this->lastmod = date('c', $lastmod); + return $this; + } + + public function changefreq(?string $changefreq = null): SitemapUrl|string + { + if ($changefreq === null) { + return $this->changefreq; + } + + $this->changefreq = $changefreq; + return $this; + } + + public function priority(?string $priority = null): SitemapUrl|string + { + if ($priority === null) { + return $this->priority; + } + + $this->priority = $priority; + return $this; + } + + public function alternates(array $alternates = []): SitemapUrl|array + { + if (empty($alternates)) { + return $this->alternates; + } + + foreach ($alternates as $alternate) { + foreach (['href', 'hreflang'] as $key) { + if (!array_key_exists($key, $alternate)) { + new Exception("[Kirby SEO] The alternate link to '{$this->loc()} is missing the '{$key}' attribute"); + } + } + } + + + $this->alternates = $alternates; + return $this; + } + + public function toDOMNode(DOMDocument $doc = new DOMDocument('1.0', 'UTF-8')): DOMNode + { + $doc->formatOutput = true; + + $node = $doc->createElement('url'); + + foreach (array_diff_key(get_object_vars($this), array_flip(['alternates'])) as $key => $value) { + $node->appendChild($doc->createElement($key, $value)); + } + + if (!empty($this->alternates())) { + foreach ($this->alternates() as $alternate) { + $alternateNode = $doc->createElement('xhtml:link'); + foreach ($alternate as $key => $value) { + $alternateNode->setAttribute($key, $value); + } + + $node->appendChild($alternateNode); + } + } + + return $node; + } + + public function toString(): string + { + $doc = new DOMDocument('1.0', 'UTF-8'); + $doc->formatOutput = true; + + $node = $this->toDOMNode(); + $doc->appendChild($node); + + return $doc->saveXML($node); + } +} diff --git a/site/plugins/kirby-seo/composer.json b/site/plugins/kirby-seo/composer.json new file mode 100644 index 0000000..6b77d04 --- /dev/null +++ b/site/plugins/kirby-seo/composer.json @@ -0,0 +1,49 @@ +{ + "name": "tobimori/kirby-seo", + "description": "The default choice for SEO on Kirby: Implement technical SEO & Meta best practices with ease and provide an easy-to-use editor experience", + "type": "kirby-plugin", + "version": "2.0.0-alpha.12", + "license": "proprietary", + "homepage": "https://github.com/tobimori/kirby-seo#readme", + "authors": [ + { + "name": "Tobias Möritz", + "email": "tobias@moeritz.io" + } + ], + "autoload": { + "psr-4": { + "tobimori\\Seo\\": "classes" + } + }, + "minimum-stability": "RC", + "require": { + "php": ">=8.3.0", + "getkirby/composer-installer": "^1.2.1" + }, + "suggest": { + "tobimori/kirby-queues": "Enable background processing support", + "getkirby/cli": "Enable background processing support", + "spatie/schema-org": "Enable the Schema.org support" + }, + "require-dev": { + "getkirby/cli": "^1.8.0", + "tobimori/kirby-queues": "^1.0.0-beta.1", + "friendsofphp/php-cs-fixer": "^3.48", + "spatie/schema-org": "^3.23", + "getkirby/cms": "^5.0.0" + }, + "scripts": { + "dist": "composer install --no-dev --optimize-autoloader", + "fix": "php-cs-fixer fix" + }, + "config": { + "optimize-autoloader": true, + "allow-plugins": { + "getkirby/composer-installer": true + } + }, + "extra": { + "kirby-cms-path": false + } +} diff --git a/site/plugins/kirby-seo/config/areas.php b/site/plugins/kirby-seo/config/areas.php new file mode 100644 index 0000000..99bc0cb --- /dev/null +++ b/site/plugins/kirby-seo/config/areas.php @@ -0,0 +1,145 @@ + fn () => + [ + 'buttons' => [ + 'page.robots' => fn (Page $page) => new RobotsViewButton($page), + 'utm-share' => fn (ModelWithContent $model) => new UtmShareViewButton($model) + ], + 'drawers' => [ + 'gsc-data' => [ + 'pattern' => 'seo/gsc/data/(:all)', + 'load' => function (string $parent) { + $kirby = App::instance(); + $request = $kirby->request(); + $metric = $request->get('metric', 'clicks'); + $asc = (bool) $request->get('asc', in_array($metric, ['position', 'query']) ? 1 : 0); + $page = max(1, (int) $request->get('page', 1)); + $limit = max(1, min(100, (int) $request->get('limit', 20))); + + try { + $model = Find::parent(ltrim($parent, '/')); + } catch (\Exception $e) { + return ['component' => 'k-error-drawer', 'props' => ['message' => 'Model not found']]; + } + + $gsc = Seo::option('components.gsc'); + if (!$gsc::hasCredentials() || !$gsc::isConnected() || !$gsc::property()) { + return ['component' => 'k-error-drawer', 'props' => ['message' => 'GSC not connected']]; + } + + $title = I18n::translate('seo.sections.searchConsole.title'); + if ($model instanceof Page) { + $title .= ' · ' . $model->title()->value(); + } + + $data = $gsc::queryForModel($model, $metric, 25000, $asc); + $total = count($data); + $pageData = array_slice($data, ($page - 1) * $limit, $limit); + + // format numbers with locale + $locale = $kirby->panelLanguage(); + $number = new NumberFormatter($locale, NumberFormatter::DECIMAL); + $percent = new NumberFormatter($locale, NumberFormatter::PERCENT); + $percent->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, 1); + $percent->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 1); + $decimal = new NumberFormatter($locale, NumberFormatter::DECIMAL); + $decimal->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, 1); + $decimal->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 1); + + $rows = array_map(fn ($row) => [ + 'query' => $row['keys'][0], + 'clicks' => $number->format($row['clicks']), + 'impressions' => $number->format($row['impressions']), + 'ctr' => $percent->format($row['ctr']), + 'position' => $decimal->format($row['position']) + ], $pageData); + + return [ + 'component' => 'k-gsc-drawer', + 'props' => [ + 'title' => $title, + 'icon' => 'google', + 'parent' => $parent, + 'metric' => $metric, + 'sortAsc' => $asc, + 'page' => $page, + 'limit' => $limit, + 'total' => $total, + 'columns' => [ + 'query' => ['label' => I18n::translate('seo.sections.searchConsole.query'), 'width' => '1/2', 'mobile' => true], + 'clicks' => ['label' => I18n::translate('seo.sections.searchConsole.clicks'), 'width' => '1/8', 'align' => 'right'], + 'impressions' => ['label' => I18n::translate('seo.sections.searchConsole.impressions'), 'width' => '1/8', 'align' => 'right'], + 'ctr' => ['label' => I18n::translate('seo.sections.searchConsole.ctr'), 'width' => '1/8', 'align' => 'right'], + 'position' => ['label' => I18n::translate('seo.sections.searchConsole.position'), 'width' => '1/8', 'align' => 'right', 'mobile' => true] + ], + 'rows' => $rows + ] + ]; + } + ] + ], + 'dialogs' => [ + 'utm-share' => [ + 'pattern' => 'seo/utm-share/(:all)', + 'controller' => UtmShareDialog::class + ], + 'gsc-select-property' => [ + 'pattern' => 'seo/gsc/select-property', + 'load' => function () { + $siteUrl = App::instance()->site()->url(); + $gsc = Seo::option('components.gsc'); + + $properties = $gsc::listProperties(); + $options = array_map(fn ($p) => [ + 'value' => $p['siteUrl'], + 'text' => str_starts_with($p['siteUrl'], 'sc-domain:') + ? substr($p['siteUrl'], 10) . ' (' . I18n::translate('seo.sections.searchConsole.scDomain') . ')' + : $p['siteUrl'] + ], $properties); + + $currentProperty = $gsc::property(); + $defaultProperty = $currentProperty ?? $gsc::findMatchingProperty($siteUrl); + + return [ + 'component' => 'k-form-dialog', + 'props' => [ + 'fields' => [ + 'property' => [ + 'label' => I18n::translate('seo.sections.searchConsole.selectPropertyLabel'), + 'type' => 'select', + 'required' => true, + 'options' => $options, + 'empty' => false + ] + ], + 'submitButton' => I18n::translate('select'), + 'value' => [ + 'property' => $defaultProperty + ] + ] + ]; + }, + 'submit' => function () { + $property = App::instance()->request()->get('property'); + Seo::option('components.gsc')::setProperty($property); + + return [ + 'event' => 'gsc.propertySelected' + ]; + } + ] + ] + ] +]; diff --git a/site/plugins/kirby-seo/config/fields.php b/site/plugins/kirby-seo/config/fields.php new file mode 100644 index 0000000..d9accf7 --- /dev/null +++ b/site/plugins/kirby-seo/config/fields.php @@ -0,0 +1,130 @@ + [ + 'extends' => 'writer', + 'props' => [ + /** + * Enables/disables the character counter in the top right corner + */ + 'ai' => function (string|bool $ai = false) { + if (!Seo::option('components.ai')::enabled()) { + return false; + } + + // check ai permission @see index.php L31 + if (App::instance()->user()->role()->permissions()->for('tobimori.seo', 'ai') === false) { + return false; + } + + return $ai; + }, + + // reset defaults + 'counter' => fn (bool $counter = false) => $counter, // we have to disable the counter because its at the same place as our ai button + 'inline' => fn (bool $inline = true) => $inline, + 'marks' => fn (array|bool|null $marks = false) => $marks, + 'nodes' => fn (array|bool|null $nodes = false) => $nodes, + ], + 'api' => fn () => [ + [ + 'pattern' => 'ai/stream', + 'method' => 'POST', + 'action' => function () { + $kirby = $this->kirby(); + $component = Seo::option('components.ai'); + + if (!$component::enabled()) { + return Response::json([ + 'status' => 'error', + 'message' => t('seo.ai.error.disabled') + ], 404); + } + + if ($kirby->user()->role()->permissions()->for('tobimori.seo', 'ai') === false) { + return Response::json([ + 'status' => 'error', + 'message' => t('seo.ai.error.permission') + ], 404); + } + + $data = $kirby->request()->body()->data(); + $lang = $kirby->api()->language(); + + // for site, use homepage + $model = $this->field()->model(); + $page = $model instanceof Page ? $model : $model->homePage(); + $kirby->site()->visit($page, $lang); + if ($lang) { + $kirby->setCurrentLanguage($lang); + } + + // inject data in snippets / rendering process + $kirby->data = [ // TODO: check if we want to access the draft / edited version for $page + 'page' => $page, + 'site' => $kirby->site(), + 'kirby' => $kirby + ]; + + // begin streaming thingy + ignore_user_abort(true); + @set_time_limit(0); + + while (ob_get_level() > 0) { + ob_end_flush(); + } + + header('Content-Type: text/event-stream'); + header('Cache-Control: no-cache'); + header('Connection: keep-alive'); + header('X-Accel-Buffering: no'); + echo ":ok\n\n"; + flush(); + + $send = static function (array $event): void { + echo 'data: ' . json_encode( + $event, + JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES + ) . "\n\n"; + + if (ob_get_level() > 0) { + ob_flush(); + } + + flush(); + }; + + try { + foreach ( + $component::streamTask($this->field()->ai(), [ + 'instructions' => $data['instructions'] ?? null, + 'edit' => $data['edit'] ?? null + ]) as $chunk + ) { + $send([ + 'type' => $chunk->type, + 'text' => $chunk->text, + 'payload' => $chunk->payload, + ]); + } + } catch (\Throwable $exception) { + $send([ + 'type' => 'error', + 'payload' => [ + 'message' => $exception->getMessage(), + ], + ]); + } + + exit(); + } + ] + ] + ] +]; diff --git a/site/plugins/kirby-seo/config/hooks.php b/site/plugins/kirby-seo/config/hooks.php new file mode 100644 index 0000000..ab002d3 --- /dev/null +++ b/site/plugins/kirby-seo/config/hooks.php @@ -0,0 +1,64 @@ + function (Page $newPage, Page $oldPage) { + // only inject blueprint defaults if the seo tab is present + if ($newPage->blueprint()->tab('seo')) { + $updates = A::reduce( + $newPage->kirby()->option('tobimori.seo.robots.types'), + function ($carry, $robots) use ($newPage) { + $upper = Str::ucfirst($robots); + + if ($newPage->content()->get("robots{$upper}")->value() === '') { + $carry["robots{$upper}"] = 'default'; + } + + return $carry; + }, + [] + ); + + if (A::count($updates)) { + $newPage = $newPage->update($updates, $newPage->kirby()->languageCode()); + } + } + + if (Seo::option('indexnow.enabled')) { + $indexNow = new (Seo::option('components.indexnow'))($newPage); + $indexNow->request(); + } + + return $newPage; + }, + 'page.changeStatus:after' => function (Page $newPage, Page $oldPage) { + if (Seo::option('indexnow.enabled')) { + $indexNow = new (Seo::option('components.indexnow'))($newPage); + $indexNow->request(); + } + }, + 'page.changeSlug:after' => function (Page $newPage, Page $oldPage) { + if (Seo::option('indexnow.enabled')) { + $indexNow = new (Seo::option('components.indexnow'))($newPage); + $indexNow->request(); + } + }, + 'page.render:before' => function (string $contentType, array $data, Page $page) { + if (!class_exists('Spatie\SchemaOrg\Schema')) { + return; + } + + if (option('tobimori.seo.generateSchema')) { + $page->schema('WebSite') + ->url($page->metadata()->canonicalUrl()) + ->copyrightYear(date('Y')) + ->description($page->metadata()->metaDescription()) + ->name($page->metadata()->metaTitle()) + ->headline($page->metadata()->title()); + } + }, +]; diff --git a/site/plugins/kirby-seo/config/options.php b/site/plugins/kirby-seo/config/options.php new file mode 100644 index 0000000..6962952 --- /dev/null +++ b/site/plugins/kirby-seo/config/options.php @@ -0,0 +1,112 @@ + [ + 'meta' => Meta::class, + 'ai' => Ai::class, + 'indexnow' => IndexNow::class, + 'schema' => SchemaSingleton::class, + 'gsc' => GoogleSearchConsole::class, + ], + 'cache.searchConsole' => true, + 'cache.indexnow' => true, + 'cascade' => [ + 'fields', + 'programmatic', + // 'fallbackFields', // fallback to meta fields for open graph fields + 'parent', + 'site', + 'options' + ], + 'default' => [ // default field values for metadata, format is [field => value] + 'metaTitle' => fn (Page $page) => $page->title(), + 'metaTemplate' => '{{ title }} - {{ site.title }}', + 'ogTemplate' => '{{ title }}', + 'ogSiteName' => fn (Page $page) => $page->site()->title(), + 'ogType' => 'website', + 'ogDescription' => fn (Page $page) => $page->metadata()->metaDescription(), + 'cropOgImage' => true, + 'locale' => fn (Page $page) => $page->kirby()->language()?->locale(LC_ALL) ?? Seo::option('locale', 'en_US'), + // default for robots: noIndex if global index configuration is set, otherwise fall back to page status + 'robotsIndex' => function (Page $page) { + $index = Seo::option('robots.index'); + if (!$index) { + return false; + } + + return Seo::option('robots.followPageStatus') ? $page->isListed() : true; + }, + 'robotsFollow' => fn (Page $page) => $page->kirby()->option('tobimori.seo.default.robotsIndex')($page), + 'robotsArchive' => fn (Page $page) => $page->kirby()->option('tobimori.seo.default.robotsIndex')($page), + 'robotsImageindex' => fn (Page $page) => $page->kirby()->option('tobimori.seo.default.robotsIndex')($page), + 'robotsSnippet' => fn (Page $page) => $page->kirby()->option('tobimori.seo.default.robotsIndex')($page), + ], + 'socialMedia' => [ // default fields for social media links, format is [field => placeholder] + 'twitter' => 'https://twitter.com/my-company', + 'facebook' => 'https://facebook.com/my-company', + 'instagram' => 'https://instagram.com/my-company', + 'youtube' => 'https://youtube.com/channel/my-company', + 'linkedin' => 'https://linkedin.com/company/my-company', + 'bluesky' => 'https://bsky.app/profile/example.bsky.social', + 'mastodon' => 'https://mastodon.social/@example' + ], + 'previews' => [ + 'google', + 'facebook', + 'slack' + ], + 'robots' => [ + 'enabled' => true, // whether robots handling should be done by the plugin + + // @deprecated - please use robots.enabled + 'active' => fn () => Seo::option('sitemap.enabled'), + 'followPageStatus' => true, // should unlisted pages be noindex by default? + 'pageSettings' => true, // whether to have robots settings on each page + 'index' => fn () => !App::instance()->option('debug'), // default site-wide robots setting + 'sitemap' => null, // sets sitemap url, will be replaced by plugin sitemap in the future + 'content' => [], // custom robots content + 'types' => ['index', 'follow', 'archive', 'imageindex', 'snippet'] // available robots types + ], + 'sitemap' => [ + 'enabled' => true, + // @deprecated - please use sitemap.enabled + 'active' => fn () => Seo::option('sitemap.enabled'), + 'redirect' => true, // redirect /sitemap to /sitemap.xml + 'locale' => 'en', + 'generator' => require __DIR__ . '/options/sitemap.php', + 'changefreq' => 'weekly', + 'groupByTemplate' => false, + 'excludeTemplates' => ['error'], + 'priority' => fn (Page $page) => number_format(($page->isHomePage()) ? 1 : max(1 - 0.2 * $page->depth(), 0.2), 1), + ], + 'files' => [ + 'parent' => null, + 'template' => null, + ], + 'canonical' => [ + 'base' => null, // base url for canonical links + 'trailingSlash' => false, // whether to add trailing slashes to canonical URLs (except for files) + ], + 'ai' => require __DIR__ . '/options/ai.php', + 'indexnow' => require __DIR__ . '/options/indexnow.php', + 'searchConsole' => [ + 'enabled' => true, + 'credentials' => null, + 'tokenPath' => fn () => App::instance()->root('config') . '/.gsc-tokens.json' + ], + 'generateSchema' => true, // whether to generate default schema.org data + 'locale' => 'en_US', // default locale, used for single-language sites + 'dateFormat' => null, // custom date format +]; diff --git a/site/plugins/kirby-seo/config/options/ai.php b/site/plugins/kirby-seo/config/options/ai.php new file mode 100644 index 0000000..2e290b6 --- /dev/null +++ b/site/plugins/kirby-seo/config/options/ai.php @@ -0,0 +1,32 @@ + true, + 'provider' => 'openai', + 'providers' => [ + 'openai' => [ + 'driver' => OpenAi::class, + 'config' => [ + 'apiKey' => '', // needs to be defined + ], + ], + 'anthropic' => [ + 'driver' => Anthropic::class, + 'config' => [ + 'apiKey' => '', // needs to be defined + ], + ], + 'openrouter' => [ + 'driver' => OpenAi::class, + 'config' => [ + 'apiKey' => '', // needs to be defined + 'model' => 'openai/gpt-5-nano', + 'endpoint' => 'https://openrouter.ai/api/v1/responses', + ], + ], + ], +]; diff --git a/site/plugins/kirby-seo/config/options/indexnow.php b/site/plugins/kirby-seo/config/options/indexnow.php new file mode 100644 index 0000000..5e0d151 --- /dev/null +++ b/site/plugins/kirby-seo/config/options/indexnow.php @@ -0,0 +1,29 @@ + true, + 'searchEngine' => 'https://api.indexnow.org', // one will propagate to all others. so this is fine @see https://www.indexnow.org/faq + 'rules' => [ + // by default, only the current page is requested to be indexed (if indexable: robots allow + listed status) + // however you might want to index other pages as well. for example, the 'blog overview' page should always be reindexed when a new 'blog post' is indexed + // + // syntax: 'match pattern' => ['invalidation rules'] + // + // match patterns: + // - '/blog/*' - url pattern (glob or regex) + // - 'article' - template name + // - '*' - wildcard, matches all pages + // + // invalidation rules: + // - 'parent' => true (direct parent) or number (levels up) + // - 'children' => true (all descendants) or number (depth limit) + // - 'siblings' => true (all siblings at same level) + // - 'urls' => ['/shop', '/'] (specific urls to invalidate) + // - 'templates' => ['category', 'shop'] (invalidate all pages with these templates) + // + // examples: + // '/blog/*' => ['parent' => true], + // 'article' => ['parent' => 2, 'urls' => ['/blog', '/']], + // 'product' => ['parent' => true, 'siblings' => true, 'templates' => ['category']], + ], +]; diff --git a/site/plugins/kirby-seo/config/options/sitemap.php b/site/plugins/kirby-seo/config/options/sitemap.php new file mode 100644 index 0000000..cef6704 --- /dev/null +++ b/site/plugins/kirby-seo/config/options/sitemap.php @@ -0,0 +1,55 @@ +index()->filter(fn ($page) => $page->metadata()->robotsIndex()->toBool() && !in_array($page->intendedTemplate()->name(), $exclude)); + + if ($group = option('tobimori.seo.sitemap.groupByTemplate')) { + $pages = $pages->group('intendedTemplate'); + } + + if (is_a($pages->first(), 'Kirby\Cms\Page')) { + $pages = $pages->group(fn () => 'pages'); + } + + foreach ($pages as $group) { + $index = $sitemap->create($group ? $group->first()->intendedTemplate()->name() : 'pages'); + + foreach ($group as $page) { + $url = $index->createUrl($page->metadata()->canonicalUrl()) + ->lastmod($page->modified() ?? (int)(date('c'))) + ->changefreq(is_callable($changefreq = option('tobimori.seo.sitemap.changefreq')) ? $changefreq($page) : $changefreq) + ->priority(is_callable($priority = option('tobimori.seo.sitemap.priority')) ? $priority($page) : $priority); + + if (kirby()->languages()->count() > 1 && kirby()->language() !== null) { + $alternates = []; + foreach (kirby()->languages() as $language) { + // only if this language is translated for this page and exists + if ($page->translation($language->code())->exists()) { + /* + * Specification: "lists every alternate version of the page, including itself." + * https://developers.google.com/search/docs/specialty/international/localized-versions#sitemap + */ + $alternates[] = + [ + 'hreflang' => Meta::toBCP47($language), + 'href' => $page->url($language->code()), + ]; + } + } + + // add x-default + $alternates[] = + [ + 'hreflang' => 'x-default', + 'href' => $page->indexUrl(), + ]; + + $url->alternates($alternates); + } + } + } +}; diff --git a/site/plugins/kirby-seo/config/page-methods.php b/site/plugins/kirby-seo/config/page-methods.php new file mode 100644 index 0000000..5198080 --- /dev/null +++ b/site/plugins/kirby-seo/config/page-methods.php @@ -0,0 +1,40 @@ + fn ($type) => Seo::option('components.schema')::getInstance($type, $this), + 'schemas' => fn () => Seo::option('components.schema')::getInstances($this), + 'metadata' => fn (?Language $lang = null) => new (Seo::option('components.meta'))($this, $lang), + 'robots' => fn (?Language $lang = null) => $this->metadata($lang)->robots(), + 'indexUrl' => function () { + // Google: "fallback page for unmatched languages, especially on language/country selectors or auto-redirecting home pages." + // https://developers.google.com/search/docs/specialty/international/localized-versions#all-method-guidelines + + // returns the index URL of the site, e.g. https://example.com/ + $kirbyUrl = $this->kirby()->url('index'); + + $defaultLang = $this->kirby()->defaultLanguage()?->code(); + // returns the site URL, e.g. https://example.com/en + // we have to request the default language so we don't get localized slugs + $siteUrl = $this->site()->url($defaultLang); + + // returns the full URL of the current page in the default language, e.g. https://example.com/en/about + // again, request default language otherwise there is a mismatch in language prefix between the site URL and the current page URL + $thisUrl = $this->url($defaultLang); + + // remove the part form the URL that is specific to the 'site' + // this is usually the language code prefix + // https://example.com/en/ + https://example.com/en/about -> https://example.com/about + if (strpos($siteUrl, $kirbyUrl) === 0 && strlen($siteUrl) > strlen($kirbyUrl)) { + if (strpos($thisUrl, $siteUrl) === 0) { + $pathAfterSite = substr($thisUrl, strlen($siteUrl)); + return "{$kirbyUrl}{$pathAfterSite}"; + } + } + + return $thisUrl; + }, +]; diff --git a/site/plugins/kirby-seo/config/routes.php b/site/plugins/kirby-seo/config/routes.php new file mode 100644 index 0000000..280a433 --- /dev/null +++ b/site/plugins/kirby-seo/config/routes.php @@ -0,0 +1,281 @@ + 'indexnow-(:any).txt', + 'method' => 'GET', + 'action' => function (string $key) { + if (Seo::option('indexnow.enabled') && Seo::option('components.indexnow')::verifyKey($key)) { + return new Response($key, 'text/plain', 200); + } + + $this->next(); + } + ], + + [ + 'pattern' => 'robots.txt', + 'method' => 'GET|HEAD', + 'action' => function () { + if (Seo::option('robots.active')) { + $content = snippet('seo/robots.txt', [], true); + return new Response($content, 'text/plain', 200); + } + + $this->next(); + } + ], + [ + 'pattern' => 'robots.txt', + 'method' => 'OPTIONS', + 'action' => function () { + if (Seo::option('robots.active')) { + return new Response('', 'text/plain', 204, ['Allow' => 'GET, HEAD']); + } + + $this->next(); + } + ], + [ + 'pattern' => 'robots.txt', + 'method' => 'ALL', + 'action' => function () { + if (Seo::option('robots.active')) { + return new Response('Method Not Allowed', 'text/plain', 405, ['Allow' => 'GET, HEAD']); + } + + $this->next(); + } + ], + + [ + 'pattern' => 'sitemap', + 'method' => 'GET|HEAD', + 'action' => function () { + if (!Seo::option('sitemap.redirect') || !Seo::option('sitemap.active')) { + $this->next(); + } + + go('/sitemap.xml'); + } + ], + [ + 'pattern' => 'sitemap', + 'method' => 'OPTIONS', + 'action' => function () { + if (Seo::option('sitemap.active')) { + return new Response('', 'text/plain', 204, ['Allow' => 'GET, HEAD']); + } + + $this->next(); + } + ], + [ + 'pattern' => 'sitemap', + 'method' => 'ALL', + 'action' => function () { + if (Seo::option('sitemap.active')) { + return new Response('Method Not Allowed', 'text/plain', 405, ['Allow' => 'GET, HEAD']); + } + + $this->next(); + } + ], + + [ + 'pattern' => 'sitemap.xsl', + 'method' => 'GET', + 'action' => function () { + if (!Seo::option('sitemap.active')) { + $this->next(); + } + + kirby()->response()->type('text/xsl'); + + $lang = Seo::option('sitemap.locale', 'en'); + kirby()->setCurrentTranslation($lang); + + return Page::factory([ + 'slug' => 'sitemap', + 'template' => 'sitemap', + 'model' => 'sitemap', + 'content' => [ + 'title' => t('sitemap'), + ], + ])->render(contentType: 'xsl'); + } + ], + [ + 'pattern' => 'sitemap.xsl', + 'method' => 'OPTIONS', + 'action' => function () { + if (Seo::option('sitemap.active')) { + return new Response('', 'text/plain', 204, ['Allow' => 'GET']); + } + + $this->next(); + } + ], + [ + 'pattern' => 'sitemap.xsl', + 'method' => 'ALL', + 'action' => function () { + if (Seo::option('sitemap.active')) { + return new Response('Method Not Allowed', 'text/plain', 405, ['Allow' => 'GET']); + } + + $this->next(); + } + ], + + [ + 'pattern' => 'sitemap.xml', + 'method' => 'GET|HEAD', + 'action' => function () { + if (!Seo::option('sitemap.active', true)) { + $this->next(); + } + + SitemapIndex::instance()->generate(); + kirby()->response()->type('text/xml'); + return Page::factory([ + 'slug' => 'sitemap', + 'template' => 'sitemap', + 'model' => 'sitemap', + 'content' => [ + 'title' => t('sitemap'), + 'index' => null, + ], + ])->render(contentType: 'xml'); + } + ], + [ + 'pattern' => 'sitemap.xml', + 'method' => 'OPTIONS', + 'action' => function () { + if (Seo::option('sitemap.active', true)) { + return new Response('', 'text/plain', 204, ['Allow' => 'GET, HEAD']); + } + + $this->next(); + } + ], + [ + 'pattern' => 'sitemap.xml', + 'method' => 'ALL', + 'action' => function () { + if (Seo::option('sitemap.active', true)) { + return new Response('Method Not Allowed', 'text/plain', 405, ['Allow' => 'GET, HEAD']); + } + + $this->next(); + } + ], + + [ + 'pattern' => 'sitemap-(:any).xml', + 'method' => 'GET|HEAD', + 'action' => function (string $index) { + if (!Seo::option('sitemap.active', true)) { + $this->next(); + } + + SitemapIndex::instance()->generate(); + if (!SitemapIndex::instance()->isValidIndex($index)) { + $this->next(); + } + + kirby()->response()->type('text/xml'); + return Page::factory([ + 'slug' => "sitemap-{$index}", + 'template' => 'sitemap', + 'model' => 'sitemap', + 'content' => [ + 'title' => t('sitemap'), + 'index' => $index, + ], + ])->render(contentType: 'xml'); + } + ], + [ + 'pattern' => 'sitemap-(:any).xml', + 'method' => 'OPTIONS', + 'action' => function () { + if (Seo::option('sitemap.active')) { + return new Response('', 'text/plain', 204, ['Allow' => 'GET, HEAD']); + } + + $this->next(); + } + ], + [ + 'pattern' => 'sitemap-(:any).xml', + 'method' => 'ALL', + 'action' => function () { + if (Seo::option('sitemap.active')) { + return new Response('Method Not Allowed', 'text/plain', 405, ['Allow' => 'GET, HEAD']); + } + + $this->next(); + } + ], + + // Google Search Console OAuth + [ + 'pattern' => '__seo/gsc/auth', + 'method' => 'GET', + 'action' => function () { + $kirby = App::instance(); + if (!$kirby->user() || !Seo::option('searchConsole.enabled') || !Seo::option('components.gsc')::hasCredentials()) { + go($kirby->site()->panel()->url()); + } + + $return = $kirby->request()->get('return') ?? $kirby->site()->panel()->url(); + $state = base64_encode(Json::encode([ + 'csrf' => bin2hex(random_bytes(16)), + 'return' => $return + ])); + + $redirectUri = rtrim($kirby->url(), '/') . '/__seo/gsc/callback'; + go(Seo::option('components.gsc')::authUrl($redirectUri, $state)); + } + ], + [ + 'pattern' => '__seo/gsc/callback', + 'method' => 'GET', + 'action' => function () { + $kirby = App::instance(); + if (!$kirby->user()) { + go($kirby->site()->panel()->url()); + } + + $request = $kirby->request(); + $state = Json::decode(base64_decode($request->get('state'))); + if (!$state || empty($state['csrf'])) { + throw new \Exception('Invalid OAuth state'); + } + + if ($error = $request->get('error')) { + throw new \Exception("OAuth error: {$error}"); + } + + if (!($code = $request->get('code'))) { + throw new \Exception('No authorization code received'); + } + + $redirectUri = rtrim($kirby->url(), '/') . '/__seo/gsc/callback'; + Seo::option('components.gsc')::exchangeCode($code, $redirectUri); + + // redirect back to where the user came from + $return = $state['return'] ?? $kirby->site()->panel()->url(); + go($return); + } + ], +]; diff --git a/site/plugins/kirby-seo/config/sections.php b/site/plugins/kirby-seo/config/sections.php new file mode 100644 index 0000000..ea4ddb8 --- /dev/null +++ b/site/plugins/kirby-seo/config/sections.php @@ -0,0 +1,134 @@ + [ + 'mixins' => ['headline'], + 'computed' => [ + 'options' => fn () => A::map(option('tobimori.seo.previews'), fn ($item) => [ + 'value' => $item, + 'text' => t("seo.sections.preview.{$item}") + ]), + 'meta' => function () { + $model = $this->model(); + + if ($model instanceof Site || $model instanceof Page) { + // clone the model with the content from the changes version + $changesVersion = $model->version('changes'); + if ($changesVersion->exists('current')) { + $model = $model->clone(['content' => $changesVersion->content()->toArray()]); + } + + // if it's a site, fall back to the home page for preview data + $model = $model instanceof Site ? $model->homePage() : $model; + if (!$model) { + return null; + } + + $meta = $model->metadata(); + return [ + 'page' => $model->slug(), + 'url' => $model->url(), + 'pageTitle' => Str::unhtml($model->title()->value()), + 'title' => Str::unhtml($meta->metaTitle()->value()), + 'description' => Str::unhtml($meta->metaDescription()->value()), + 'ogSiteName' => Str::unhtml($meta->ogSiteName()->value()), + 'ogTitle' => Str::unhtml($meta->ogTitle()->value()), + 'ogDescription' => Str::unhtml($meta->ogDescription()->value()), + 'ogImage' => $meta->ogImage(), + 'cropOgImage' => $meta->cropOgImage()->toBool(), + 'panelUrl' => method_exists($model, 'panel') ? "{$model->panel()?->url()}?tab=seo" : null, + ]; + } + + return null; + } + ] + ], + 'heading-structure' => [ + 'mixins' => ['headline'], + 'computed' => [ + 'data' => function () { + $model = $this->model(); + if (!($model instanceof Page)) { + // only works for pages (not site, files, etc.) + return []; + } + + // In Kirby 5, use the changes version if it exists + // clone the model with the content from the changes version + $changesVersion = $model->version('changes'); + if ($changesVersion->exists('current')) { + $model = $model->clone(['content' => $changesVersion->content()->toArray()]); + } + + // Render the page + $page = $model->render(); + $dom = new DOMDocument(); + $dom->loadHTML(htmlspecialchars_decode(mb_convert_encoding(htmlentities($page, ENT_COMPAT, 'UTF-8'), 'ISO-8859-1', 'UTF-8'), ENT_QUOTES), libxml_use_internal_errors(true)); + + $xpath = new DOMXPath($dom); + $headings = $xpath->query('//h1|//h2|//h3|//h4|//h5|//h6'); + $data = []; + + foreach ($headings as $heading) { + $data[] = [ + 'level' => (int)str_replace('h', '', $heading->nodeName), + 'text' => $heading->textContent, + ]; + } + + return $data; + } + ] + ], + 'seo-search-console' => [ + 'mixins' => ['headline'], + 'computed' => [ + 'status' => function () { + if (!Seo::option('components.gsc')::hasCredentials()) { + return 'NO_CREDENTIALS'; + } + + if (!Seo::option('components.gsc')::isConnected()) { + return 'NOT_CONNECTED'; + } + + if (!Seo::option('components.gsc')::property()) { + return 'SELECT_PROPERTY'; + } + + return 'CONNECTED'; + }, + 'property' => fn () => Seo::option('components.gsc')::property(), + 'pageUrl' => function () { + $model = $this->model(); + if ($model instanceof Page) { + return '/' . $model->uri(); + } + return null; + }, + 'data' => function () { + $gsc = Seo::option('components.gsc'); + if (!$gsc::hasCredentials() || !$gsc::isConnected() || !$gsc::property()) { + return []; + } + + $metric = kirby()->request()->get('metric', 'clicks'); + $limit = (int) kirby()->request()->get('limit', 10); + $asc = in_array($metric, ['position', 'query']); + + try { + return $gsc::queryForModel($this->model(), $metric, $limit, $asc); + } catch (\Exception $e) { + return []; + } + } + ] + ] +]; diff --git a/site/plugins/kirby-seo/config/site-methods.php b/site/plugins/kirby-seo/config/site-methods.php new file mode 100644 index 0000000..cdeb364 --- /dev/null +++ b/site/plugins/kirby-seo/config/site-methods.php @@ -0,0 +1,48 @@ + fn ($type) => Seo::option('components.schema')::getInstance($type), + 'schemas' => fn () => Seo::option('components.schema')::getInstances(), + 'lang' => fn () => Seo::option('components.meta')::normalizeLocale(Seo::option('default.locale', args: [$this->homePage()]), '-'), + 'canonicalFor' => function (string $url, bool $useRootUrl = false) { + // Determine the base URL + $base = Seo::option('canonical.base', Seo::option('canonicalBase')); + if (!$base) { + // If useRootUrl is true or this is a multilang site requesting root URL, use kirby()->url() + if ($useRootUrl && kirby()->multilang()) { + $base = kirby()->url(); + } else { + $base = $this->url(); + } + } + + if (Str::startsWith($url, $base)) { + $canonicalUrl = $url; + } else { + $path = Url::path($url); + $canonicalUrl = url($base . '/' . $path); + } + + $trailingSlash = Seo::option('canonical.trailingSlash', false); + if ($trailingSlash) { + // check if URL has a file extension (like .xml, .jpg, .pdf, etc.) + $path = parse_url($canonicalUrl, PHP_URL_PATH) ?? ''; + $pathInfo = pathinfo($path); + $hasExtension = !empty($pathInfo['extension'] ?? null); + + // Only add trailing slash if: + // - URL doesn't already have one + // - URL doesn't have a file extension + // - URL isn't just the base domain + if (!Str::endsWith($canonicalUrl, '/') && !$hasExtension && $canonicalUrl !== $base) { + $canonicalUrl .= '/'; + } + } + + return $canonicalUrl; + } +]; diff --git a/site/plugins/kirby-seo/index.css b/site/plugins/kirby-seo/index.css new file mode 100644 index 0000000..7f1e52e --- /dev/null +++ b/site/plugins/kirby-seo/index.css @@ -0,0 +1 @@ +.k-seo-template-variable{color:light-dark(var(--theme-color-text),var(--theme-color-back));background:color-mix(in srgb,var(--theme-color-back) 25%,var(--input-color-back));border-radius:var(--button-rounded);display:inline-block;font-weight:var(--font-semi);height:var(--text-line-height) em;line-height:var(--text-line-height) em;padding-inline:.5rem;white-space:nowrap;-webkit-user-select:none;user-select:none;vertical-align:baseline}.k-field-type-seo-writer img.ProseMirror-separator{display:inline-block;width:0;height:0;margin:0;padding:0;border:0;overflow:hidden}.k-field-type-seo-writer br.ProseMirror-trailingBreak{display:none}.k-field-type-seo-writer .k-writer-input .k-toolbar-button{padding-inline:var(--spacing-5);--button-width: auto;flex-basis:max-content}.k-field-type-seo-writer .k-writer-input .k-toolbar-button:after{content:attr(title)}.k-field-type-seo-writer .k-writer-input .k-toolbar-button:not(:first-child){border-left:1px solid var(--toolbar-border)}.k-seo-utm-share-dialog__close{position:absolute;top:var(--spacing-2);right:var(--spacing-2);z-index:1}.k-seo-utm-share-dialog__url-wrapper{margin-bottom:var(--spacing-6)}.k-seo-utm-share-dialog__section-label{margin-bottom:var(--spacing-2)}.k-seo-utm-share-dialog__url{padding-right:var(--spacing-1)}.k-seo-utm-share-dialog__copy{--button-height: calc(var(--input-height) - var(--spacing-2));--button-rounded: var(--rounded-sm);flex-shrink:0}.k-seo-utm-share-dialog__params{display:flex;flex-direction:column;gap:var(--spacing-2)}.k-seo-utm-share-dialog__row{display:flex;align-items:center;background:light-dark(var(--color-gray-100),var(--color-gray-900));border:1px solid var(--color-border);border-radius:var(--rounded)}.k-seo-utm-share-dialog__row .k-input,.k-seo-utm-share-dialog__row .k-string-input{border-top-left-radius:0;border-bottom-left-radius:0}.k-seo-utm-share-dialog__row .k-input{--input-color-back: light-dark(var(--color-white), var(--color-gray-850))}.k-seo-utm-share-dialog__label{display:flex;align-items:center;gap:var(--spacing-2);width:7rem;flex-shrink:0;padding:var(--input-padding);font-size:var(--text-sm);color:var(--color-text)}.k-seo-utm-share-dialog__label .k-icon{color:var(--color-text-dimmed)}.k-seo-utm-share-dialog__input{flex:1}.k-gsc-drawer .k-table th{cursor:pointer}.k-gsc-drawer .k-table th:hover{background:light-dark(var(--color-gray-200),var(--color-gray-700))}.k-gsc-drawer .k-table th>span{display:flex;align-items:center;justify-content:space-between;width:100%}.k-heading-structure__label{display:flex;align-items:center;justify-content:flex-start;gap:var(--spacing-2)}.k-heading-structure__label>.k-icon{color:var(--color-gray-700)}.k-heading-structure__label>.k-loader{margin-left:auto;color:var(--color-gray-700)}.k-heading-structure__notice{margin-top:var(--spacing-2);display:flex;align-items:flex-start}.k-heading-structure__notice>.k-icon{margin-top:.125rem;margin-right:var(--spacing-1);color:var(--color-red)}.k-heading-structure__list{overflow:hidden}.k-heading-structure__item{position:relative;background:var(--theme-color-back);padding-block:var(--spacing-px);display:flex}.k-heading-structure__item.is-invalid{color:var(--color-red)}.k-heading-structure__item__level{font-family:var(--font-mono);font-weight:700;margin-right:var(--spacing-2)}.k-heading-structure__item__text{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.k-heading-structure__item.level-2{margin-left:0;padding-left:1.6rem}.k-heading-structure__item.level-2:before{content:"";position:absolute;top:calc(50% - .0625rem);left:.4rem;width:.8rem;height:.125rem;background-color:currentColor}.k-heading-structure__item.level-2:after{content:"";position:absolute;bottom:calc(50% - .0625rem);left:.4rem;height:9999px;width:.125rem;background-color:currentColor}.k-heading-structure__item.level-3{margin-left:1.6rem;padding-left:1.6rem}.k-heading-structure__item.level-3:before{content:"";position:absolute;top:calc(50% - .0625rem);left:.4rem;width:.8rem;height:.125rem;background-color:currentColor}.k-heading-structure__item.level-3:after{content:"";position:absolute;bottom:calc(50% - .0625rem);left:.4rem;height:9999px;width:.125rem;background-color:currentColor}.k-heading-structure__item.level-4{margin-left:3.2rem;padding-left:1.6rem}.k-heading-structure__item.level-4:before{content:"";position:absolute;top:calc(50% - .0625rem);left:.4rem;width:.8rem;height:.125rem;background-color:currentColor}.k-heading-structure__item.level-4:after{content:"";position:absolute;bottom:calc(50% - .0625rem);left:.4rem;height:9999px;width:.125rem;background-color:currentColor}.k-heading-structure__item.level-5{margin-left:4.8rem;padding-left:1.6rem}.k-heading-structure__item.level-5:before{content:"";position:absolute;top:calc(50% - .0625rem);left:.4rem;width:.8rem;height:.125rem;background-color:currentColor}.k-heading-structure__item.level-5:after{content:"";position:absolute;bottom:calc(50% - .0625rem);left:.4rem;height:9999px;width:.125rem;background-color:currentColor}.k-heading-structure__item.level-6{margin-left:6.4rem;padding-left:1.6rem}.k-heading-structure__item.level-6:before{content:"";position:absolute;top:calc(50% - .0625rem);left:.4rem;width:.8rem;height:.125rem;background-color:currentColor}.k-heading-structure__item.level-6:after{content:"";position:absolute;bottom:calc(50% - .0625rem);left:.4rem;height:9999px;width:.125rem;background-color:currentColor}.k-facebook-preview{background:light-dark(#ffffff,#242526);border:1px solid light-dark(#dadde1,#3e4042);overflow:hidden;border-radius:var(--rounded)}.k-facebook-preview__image{width:100%;height:0;padding-bottom:52.355%;position:relative}.k-facebook-preview__image img{position:absolute;top:0;left:0;width:100%;height:100%;object-fit:cover}.k-facebook-preview__content{padding:.75rem 1rem;background:light-dark(#f0f2f5,#363638)}.k-facebook-preview__title,.k-facebook-preview__description,.k-facebook-preview__url{display:-webkit-box;-webkit-box-orient:vertical;overflow:hidden;-webkit-line-clamp:1}.k-facebook-preview__url{color:light-dark(#65676b,#b0b3b8);font-size:.75rem;text-transform:uppercase;line-height:1.1;margin-bottom:.25rem}.k-facebook-preview__title{font-weight:600;line-height:1.1765;font-size:1rem;color:light-dark(#050505,#e4e6eb);margin:.3125rem 0}.k-facebook-preview__description{line-height:1.3333;color:light-dark(#65676b,#b0b3b8);font-size:.875rem}.k-google-search-preview{padding:1rem;background:var(--input-color-back);border-radius:var(--input-rounded);overflow:hidden}.k-google-search-preview__header{display:flex;align-items:center;gap:.75rem;margin-bottom:.75rem}.k-google-search-preview__favicon{display:inline-flex;width:26px;height:26px;align-items:center;justify-content:center;border-radius:50%;border:1px solid light-dark(#ecedef,#9aa0a6);background:light-dark(#f1f3f4,#fff);margin:0}.k-google-search-preview__favicon img{display:block;width:18px;height:18px}.k-google-search-preview__site-info{display:flex;flex-direction:column;min-width:0;flex:1}.k-google-search-preview__site-title{font-size:.875rem;color:light-dark(#202124,#bdc1c6);line-height:1.2;margin-bottom:.125rem;display:block}.k-google-search-preview__url{font-size:.75rem;color:light-dark(#5f6368,#9aa0a6);line-height:1.2;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:1;overflow:hidden}.k-google-search-preview__title{margin:0;font-size:1.25rem;font-weight:400;color:light-dark(#1a0dab,#99c3ff);line-height:1.2;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:1;overflow:hidden}.k-google-search-preview__description{margin:.25rem 0 0;font-size:.875rem;color:light-dark(#4d5156,#bfbfbf);line-height:1.4;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}.k-slack-preview{max-width:32.5rem;position:relative;padding-left:1rem;line-height:1.46666667;font-size:.9375rem}.k-slack-preview:before{position:absolute;content:"";top:0;left:0;bottom:0;width:.25rem;border-radius:.5rem;background:light-dark(#ddd,#4a4b4d)}.k-slack-preview__site-name{display:flex;align-items:center;gap:.25rem;color:light-dark(#616061,#d1d2d3);font-size:.75rem;margin-bottom:.25rem}.k-slack-preview__favicon{width:16px;height:16px}.k-slack-preview__title{font-weight:700;display:block;color:light-dark(#1264a3,#1d9bd1);cursor:pointer;margin-bottom:.25rem}.k-slack-preview__title:hover{text-decoration:underline}.k-slack-preview__description{color:light-dark(#1d1c1d,#d1d2d3);margin:0 0 .25rem}.k-slack-preview__image-toggle{color:light-dark(#1264a3,#1d9bd1);margin-left:.25rem;font-size:.625rem;background:none;border:none;padding:0;cursor:pointer;font-family:inherit}.k-slack-preview__image-toggle:hover{opacity:.7}.k-slack-preview__image{border-radius:.5rem;max-width:22.5rem;overflow:hidden;position:relative}.k-slack-preview__image:before{border-radius:.5rem;content:"";top:0;right:0;bottom:0;left:0;z-index:2;position:absolute;box-shadow:inset 0 0 0 1px #0000001a}.k-slack-preview__image img{width:100%;height:100%;display:block}.k-seo-preview__inner{margin-top:var(--spacing-2)}.k-seo-preview__debugger{margin-top:var(--spacing-4);display:flex;font-size:var(--text-sm);color:var(--color-text-dimmed);line-height:var(--spacing-5);width:max-content;margin-left:auto}.k-seo-preview__debugger:hover{text-decoration:underline;color:var(--theme-color-text)}.k-seo-preview__debugger>.k-icon{margin-left:var(--spacing-2)}.k-seo-preview__label{display:flex;align-items:center;justify-content:flex-start;gap:var(--spacing-2)}.k-seo-preview__label>.k-icon{color:var(--theme-color-icon)}.k-seo-preview__panel-button{margin-left:auto}.k-search-console-empty{flex-direction:column;text-align:center;padding:var(--spacing-6)!important;gap:var(--spacing-3)}.k-search-console__inner{margin-top:var(--spacing-2)}.k-search-console__list{--table-color-back: light-dark(var(--color-white), var(--color-gray-850));--table-color-border: light-dark(rgba(0, 0, 0, .08), rgba(0, 0, 0, .375));background:var(--table-color-back);border-radius:var(--rounded);overflow:hidden;box-shadow:var(--shadow)}.k-search-console__row{position:relative;display:flex;align-items:center;justify-content:space-between;padding:var(--spacing-2) var(--spacing-3);font-size:var(--text-sm);border-bottom:1px solid var(--table-color-border)}.k-search-console__row:last-child{border-bottom:none}.k-search-console__row>.k-search-console__bar{position:absolute;top:0;bottom:0;left:0;right:auto;background:light-dark(var(--color-blue-200),var(--color-blue-300));opacity:light-dark(.75,.2);pointer-events:none}.k-search-console__row>.k-search-console__query,.k-search-console__row>.k-search-console__value{position:relative}.k-search-console__row>.k-search-console__query{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-right:var(--spacing-3)}.k-search-console__row>.k-search-console__value{font-variant-numeric:tabular-nums;color:var(--color-text-dimmed)}.k-search-console__options{margin-left:auto}.k-search-console__link{display:flex;margin-top:var(--spacing-4);margin-left:auto;width:max-content;font-size:var(--text-sm);color:var(--color-text-dimmed);line-height:var(--spacing-5)}.k-search-console__link:hover{text-decoration:underline;color:var(--theme-color-text)}.k-search-console__link>.k-icon{margin-left:var(--spacing-2)} diff --git a/site/plugins/kirby-seo/index.js b/site/plugins/kirby-seo/index.js new file mode 100644 index 0000000..a25ccb6 --- /dev/null +++ b/site/plugins/kirby-seo/index.js @@ -0,0 +1,5 @@ +(function(){"use strict";function f(o,e,t,s,a,n,u,i){var l=typeof o=="function"?o.options:o;return e&&(l.render=e,l.staticRenderFns=t,l._compiled=!0),{exports:o,options:l}}const D={extends:"k-writer-field",props:{ai:[String,Boolean]},data(){return{aiStreaming:!1,aiAbortController:null}},computed:{buttons(){if(!this.ai)return[];if(this.aiStreaming)return[{icon:"loader",text:this.$t("seo.ai.action.stop"),disabled:this.disabled||!this.aiEndpointUrl,theme:"red",click:()=>this.abortAiStream()}];const o=[{icon:this.value===""?"seo-ai":"refresh",text:this.value===""?this.$t("seo.ai.action.generate"):this.$t("seo.ai.action.regenerate"),disabled:this.disabled||!this.aiEndpointUrl,click:()=>this.startAiStream()},{icon:"cog",title:this.$t("seo.ai.action.customize"),disabled:this.disabled||!this.aiEndpointUrl,click:()=>this.openCustomizeDialog()}];return this.value!==""?[{icon:"seo-ai",text:this.$t("seo.ai.action.edit"),disabled:this.disabled||!this.aiEndpointUrl,click:()=>this.openEditDialog()},...o]:o},aiEndpointUrl(){var t,s,a;const o=(s=(t=this.$panel)==null?void 0:t.urls)==null?void 0:s.api,e=(a=this.endpoints)==null?void 0:a.field;return!o||!e?null:`${o}/${e}/ai/stream`.replace(/([^:]\/)\/+/g,"$1")}},beforeDestroy(){this.abortAiStream()},methods:{async startAiStream(o={}){var a,n,u,i;const e=this.aiEndpointUrl;if(!e||this.disabled||this.aiStreaming)return;(a=this.$refs.input)!=null&&a.focus&&this.$refs.input.focus();const t=this.getEditor();t&&t.clearContent();const s=new AbortController;this.aiAbortController=s,this.aiStreaming=!0;try{const l=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json",Accept:"text/event-stream","X-CSRF":(u=(n=this.$panel)==null?void 0:n.system)==null?void 0:u.csrf,"X-Language":(i=this.$panel)==null?void 0:i.language.code},body:JSON.stringify({instructions:o.instructions,edit:o.edit}),credentials:"same-origin",signal:s.signal});if(!l.ok)try{const d=await l.json();throw new Error((d==null?void 0:d.message)||this.$t("seo.ai.error.request"))}catch{throw new Error(this.$t("seo.ai.error.request"))}if(!l.body)throw new Error(this.$t("seo.ai.error.request"));await this.consumeStream(l.body.getReader())}catch(l){if(l&&l.name==="AbortError")return;console.error(l);const d=l&&l.message?l.message:this.$t("seo.ai.error.request");this.$panel.notification.error(d)}finally{this.aiAbortController=null,this.aiStreaming=!1}},async consumeStream(o){const e=new TextDecoder;let t="";try{for(;;){const{value:s,done:a}=await o.read();if(a)break;s&&(t+=e.decode(s,{stream:!0}),t=this.processStreamBuffer(t))}t+=e.decode(),this.processStreamBuffer(t)}finally{o&&typeof o.releaseLock=="function"&&o.releaseLock()}},processStreamBuffer(o){let e=o;for(;;){const t=e.indexOf(` + +`);if(t===-1)return e;const s=e.slice(0,t);if(e=e.slice(t+2),s.trim()==="")continue;const a=s.split(` +`).filter(n=>n.trim().startsWith("data:")).map(n=>n.trim().slice(5)).join(` +`).trim();a!==""&&this.handleAiEvent(a)}},handleAiEvent(o){let e=null;try{e=JSON.parse(o)}catch(t){console.error("Failed to parse AI chunk",t,o);return}if(e.type!=="text-start"){if(e.type==="text-delta"){this.applyAiDelta(e.text||"");return}if(e.type!=="thinking-delta"&&!(e.type==="tool-call"||e.type==="tool-result")&&e.type!=="stream-end"&&e.type==="error")throw new Error(e.payload&&e.payload.message?e.payload.message:this.$t("seo.ai.error.request"))}},applyAiDelta(o){if(!o)return;const e=this.getEditor();if(!e)return;const{state:t,view:s}=e;if(!t||!s)return;const a=t.doc.content.size,n=t.schema.text(o),u=t.tr.insert(a,n);s.dispatch(u)},getEditor(){var e,t;const o=(t=(e=this.$refs.input)==null?void 0:e.$refs)==null?void 0:t.input;return(o==null?void 0:o.editor)||null},abortAiStream(){this.aiAbortController&&(this.aiAbortController.abort(),this.aiAbortController=null),this.aiStreaming=!1},openEditDialog(){this.$panel.dialog.open({component:"k-form-dialog",props:{fields:{instructions:{label:this.$t("seo.ai.dialog.instructions.label"),type:"textarea",buttons:!1,placeholder:this.$t("seo.ai.dialog.instructions.placeholder"),required:!0}},submitButton:this.$t("seo.ai.dialog.edit.submit")},on:{submit:o=>{this.$panel.dialog.close(),this.startAiStream({edit:this.value,instructions:o.instructions})}}})},openCustomizeDialog(){this.$panel.dialog.open({component:"k-form-dialog",props:{fields:{instructions:{label:this.$t("seo.ai.dialog.custom.label"),type:"textarea",buttons:!1,placeholder:this.$t("seo.ai.dialog.custom.placeholder"),required:!0}},submitButton:this.$t("seo.ai.dialog.custom.submit")},on:{submit:o=>{this.$panel.dialog.close(),this.startAiStream({instructions:o.instructions})}}})}}};var I=function(){var e=this,t=e._self._c;return t("k-field",e._b({class:["k-writer-field",e.$attrs.class],style:e.$attrs.style,attrs:{counter:e.counterOptions,input:e.id},scopedSlots:e._u([e.disabled?null:{key:"options",fn:function(){return[t("k-button-group",{ref:"buttons",staticClass:"k-field-options",attrs:{buttons:e.buttons,layout:"collapsed",size:"xs",variant:"filled"}})]},proxy:!0}],null,!0)},"k-field",e.$props,!1),[t("k-input",e._b({ref:"input",attrs:{after:e.after,before:e.before,icon:e.icon,type:"seo-writer"},on:{input:function(s){return e.$emit("input",s)}}},"k-input",e.$props,!1))],1)},O=[],U=f(D,I,O);const F=U.exports,M={extends:"k-writer-input",methods:{createNodes(){return Vue.component("k-writer-input").options.methods.createNodes.call(this).filter(e=>e.name!=="hardBreak")}}};var B=function(){var e=this,t=e._self._c;return t("div",{directives:[{name:"direction",rawName:"v-direction"}],ref:"editor",class:["k-writer","k-writer-input",e.$attrs.class],style:e.$attrs.style,attrs:{"data-disabled":e.disabled,"data-empty":e.isEmpty,"data-placeholder":e.placeholder,spellcheck:e.spellcheck}},[e.editor&&!e.disabled?t("k-writer-toolbar",e._b({ref:"toolbar",on:{command:e.onCommand}},"k-writer-toolbar",e.toolbarOptions,!1)):e._e(),t("textarea",{ref:"output",staticClass:"input-hidden",attrs:{name:e.name,required:e.required,tabindex:"-1"},domProps:{value:e.value}})],1)},V=[],z=f(M,B,V);const H=z.exports,r=window.Vue;function y(){return window.panel}function L(){return y().api}function q(){return y().app}function R(){const o=L();return{load:({parent:t,name:s})=>o.get(`${t}/sections/${s}`)}}const m=r.computed;r.customRef,r.defineAsyncComponent,r.defineComponent,r.effectScope,r.getCurrentInstance,r.getCurrentScope,r.h,r.inject,r.isProxy,r.isReactive,r.isReadonly,r.isRef,r.isShallow,r.markRaw;const Z=r.nextTick;r.onActivated,r.onBeforeMount,r.onBeforeUnmount,r.onBeforeUpdate,r.onDeactivated,r.onErrorCaptured;const C=r.onMounted;r.onRenderTracked,r.onRenderTriggered,r.onScopeDispose,r.onServerPrefetch;const S=r.onUnmounted;r.onUpdated,r.provide,r.proxyRefs,r.reactive,r.readonly;const v=r.ref;r.shallowReactive,r.shallowReadonly,r.shallowRef,r.toRaw,r.toRef,r.toRefs,r.triggerRef,r.unref,r.useAttrs,r.useCssModule,r.useCssVars,r.useListeners,r.useSlots;const x=r.watch;r.watchEffect,r.watchPostEffect,r.watchSyncEffect;const j={__name:"utm-share-dialog",props:{pageUrl:{type:String,required:!0},visible:{type:Boolean,default:!0},size:{type:String,default:"medium"}},emits:["cancel"],setup(o,{emit:e}){const t=o,s=v({utm_source:"",utm_medium:"",utm_campaign:"",utm_content:"",utm_term:"",ref:""}),a=[{key:"utm_source",icon:"globe",name:"source"},{key:"utm_medium",icon:"dashboard",name:"medium"},{key:"utm_campaign",icon:"megaphone",name:"campaign"},{key:"utm_content",icon:"image",name:"content"},{key:"utm_term",icon:"search",name:"term"},{key:"ref",icon:"url",name:"ref"}],n=v(!1),u=v(null),i=m(()=>{const c=new URL(t.pageUrl);for(const p of a)s.value[p.key]&&c.searchParams.set(p.key,s.value[p.key]);return c.toString()}),l=async()=>{try{await navigator.clipboard.writeText(i.value),n.value=!0,setTimeout(()=>{n.value=!1},2e3)}catch(c){console.error("Failed to copy:",c)}};x(i,()=>{Z(()=>{u.value&&(u.value.scrollLeft=u.value.scrollWidth)})});const d=c=>{var p;t.visible&&(c.ctrlKey||c.metaKey)&&c.key==="c"&&!((p=window.getSelection())!=null&&p.toString())&&(c.preventDefault(),l())};return C(()=>{document.addEventListener("keydown",d)}),S(()=>{document.removeEventListener("keydown",d)}),{__sfc:!0,props:t,emit:e,params:s,fields:a,copied:n,urlInput:u,generatedUrl:i,copyToClipboard:l,handleKeydown:d}}};var W=function(){var e=this,t=e._self._c,s=e._self._setupProxy;return t("k-dialog",{staticClass:"k-seo-utm-share-dialog",attrs:{size:e.size,visible:e.visible,"cancel-button":!1,"submit-button":!1},on:{cancel:function(a){return s.emit("cancel")}},scopedSlots:e._u([{key:"header",fn:function(){return[t("k-button",{staticClass:"k-seo-utm-share-dialog__close",attrs:{icon:"cancel"},on:{click:function(a){return s.emit("cancel")}}})]},proxy:!0}])},[t("div",{staticClass:"k-seo-utm-share-dialog__url-wrapper"},[t("k-label",{staticClass:"k-seo-utm-share-dialog__section-label"},[e._v(e._s(e.$t("seo.utmShare.button")))]),t("div",{staticClass:"k-input k-seo-utm-share-dialog__url"},[t("span",{staticClass:"k-input-element"},[t("input",{ref:"urlInput",staticClass:"k-string-input",attrs:{id:"generated-url",type:"text",readonly:"","data-font":"monospace"},domProps:{value:s.generatedUrl},on:{focus:function(a){return a.target.select()}}})]),t("k-button",{staticClass:"k-seo-utm-share-dialog__copy",attrs:{icon:s.copied?"check":"copy",theme:s.copied?"positive":"notice",variant:"filled"},on:{click:s.copyToClipboard}})],1)],1),t("k-label",{staticClass:"k-seo-utm-share-dialog__section-label"},[e._v(e._s(e.$t("seo.utmShare.parameters")))]),t("div",{staticClass:"k-seo-utm-share-dialog__params"},e._l(s.fields,function(a){return t("div",{key:a.key,staticClass:"k-seo-utm-share-dialog__row"},[t("label",{staticClass:"k-seo-utm-share-dialog__label",attrs:{for:a.key}},[t("k-icon",{attrs:{type:a.icon}}),e._v(" "+e._s(e.$t(`seo.utmShare.${a.name}.label`))+" ")],1),t("k-input",{staticClass:"k-seo-utm-share-dialog__input"},[t("k-text-input",{attrs:{id:a.key,placeholder:e.$t(`seo.utmShare.${a.name}.placeholder`)},model:{value:s.params[a.key],callback:function(n){e.$set(s.params,a.key,n)},expression:"params[field.key]"}})],1)],1)}),0)],1)},G=[],K=f(j,W,G);const Y=K.exports,J={__name:"gsc-drawer",props:{columns:Object,rows:Array,parent:String,metric:String,sortAsc:Boolean,total:Number,page:Number,limit:Number,visible:Boolean,current:Boolean,icon:String,title:String,breadcrumb:Array,tabs:Object,tab:String,options:Array},emits:["cancel","crumb","submit","tab"],setup(o,{emit:e}){const t=o,s=Vue.computed(()=>({page:t.page,limit:t.limit,total:t.total,details:!0})),a=(i={})=>{window.panel.drawer.refresh({query:{metric:i.metric??t.metric,asc:i.asc??t.sortAsc?1:0,page:i.page??t.page}})};return{__sfc:!0,props:t,emit:e,pagination:s,reload:a,handlePaginate:i=>a({page:i.page}),handleHeader:({columnIndex:i})=>{const l=t.metric===i?!t.sortAsc:i==="position"||i==="query";a({metric:i,asc:l,page:1})}}}};var X=function(){var a;var e=this,t=e._self._c,s=e._self._setupProxy;return t("k-drawer",e._b({staticClass:"k-gsc-drawer",on:{cancel:function(n){return s.emit("cancel")},crumb:function(n){return s.emit("crumb",n)},submit:function(n){return s.emit("cancel")},tab:function(n){return s.emit("tab",n)}}},"k-drawer",e.$props,!1),[(a=s.props.rows)!=null&&a.length?t("k-table",{attrs:{columns:s.props.columns,rows:s.props.rows,index:!1,pagination:s.pagination},on:{header:s.handleHeader,paginate:s.handlePaginate},scopedSlots:e._u([{key:"header",fn:function({columnIndex:n,label:u}){return[t("span",[e._v(" "+e._s(u)+" "),s.props.metric===n?t("k-icon",{attrs:{type:s.props.sortAsc?"angle-up":"angle-down"}}):e._e()],1)]}}])}):t("k-box",{attrs:{theme:"empty"}},[t("k-text",[e._v(e._s(e.$t("seo.sections.searchConsole.noData")))])],1)],1)},Q=[],ee=f(J,X,Q);const te=ee.exports,A=o=>{const e={theme:"blue",...o};return{get button(){var t,s;return{id:e.name,icon:e.icon,label:(s=(t=window.panel)==null?void 0:t.$t)==null?void 0:s.call(t,e.label),name:e.name,inline:!0}},get schema(){return{group:"inline",inline:!0,atom:!0,selectable:!1,attrs:{variable:{default:e.variable}},leafText:t=>`{{ ${t.attrs.variable} }}`,parseDOM:[{tag:`span[data-seo-template-variable="${e.variable}"]`,getAttrs:t=>({variable:t.dataset.seoTemplateVariable??e.variable})}],toDOM:t=>["span",{"data-seo-template-variable":t.attrs.variable},`{{ ${t.attrs.variable} }}`]}},commands({type:t}){return()=>(s,a)=>{if(!a)return!1;const{from:n,to:u}=s.selection,i=t.create({variable:e.variable}),l=s.tr;l.delete(n,u),l.insert(n,i),l.insertText(" ",n+i.nodeSize);const d=s.selection.constructor,c=n+i.nodeSize+1;return l.setSelection(d.near(l.doc.resolve(c))),a(l.scrollIntoView()),!0}},view(t){var a,n;const s=document.createElement("span");return s.className="k-seo-template-variable",s.dataset.theme=e.theme,s.dataset.seoTemplateVariable=t.attrs.variable,s.setAttribute("contenteditable","false"),s.textContent=(n=(a=window.panel)==null?void 0:a.$t)==null?void 0:n.call(a,e.label),{dom:s,update(u){var i,l;return s.dataset.seoTemplateVariable=u.attrs.variable,s.textContent=(l=(i=window.panel)==null?void 0:i.$t)==null?void 0:l.call(i,e.label),!0},ignoreMutation:()=>!0}}}},T={blueprint:String,lock:[Boolean,Object],help:String,name:String,parent:String,timestamp:Number},se={__name:"heading-structure",props:T,setup(o){const e=o,t=y(),{load:s}=R(),a=v(null),n=m(()=>{var c;return(c=a.value)==null?void 0:c.some((p,k)=>{var _;return p.level>(((_=a.value[k-1])==null?void 0:_.level)??0)+1})}),u=m(()=>{var c;return((c=a.value)==null?void 0:c.filter(p=>p.level===1).length)>1}),i=m(()=>{var c;return((c=a.value)==null?void 0:c.filter(p=>p.level===1).length)===0}),l=()=>s({parent:e.parent,name:e.name}).then(c=>{a.value=c.data}),d=(c,p)=>{var k;return!!(c.level>(((k=a.value[p-1])==null?void 0:k.level)??0)+1||c.level===1&&a.value[p-1]||c.level===1&&a.value.filter(_=>_.level===1).length>1)};return C(()=>{l(),t.events.on("content.save",c=>{l()})}),S(()=>t.events.off("content.save")),{__sfc:!0,props:e,panel:t,load:s,data:a,incorrectOrder:n,multipleH1:u,noH1:i,handleLoad:l,itemInvalid:d}}};var ae=function(){var e=this,t=e._self._c,s=e._self._setupProxy;return s.data?t("div",{staticClass:"k-section k-heading-structure"},[t("div",{staticClass:"k-field-header k-heading-structure__label k-label k-field-label"},[t("k-icon",{attrs:{type:"headline"}}),t("span",{staticClass:"k-label-text"},[e._v(e._s(s.props.label||e.$t("seo.sections.headingStructure.title")))])],1),t("k-box",{attrs:{theme:"white"}},[t("ol",{staticClass:"k-heading-structure__list"},e._l(s.data,function(a,n){return t("li",{key:n,class:`k-heading-structure__item level-${a.level} ${s.itemInvalid(a,n)?"is-invalid":""}`,style:`z-index: ${s.data.length-n}`},[t("span",{staticClass:"k-heading-structure__item__level"},[e._v(" H"+e._s(a.level)+" ")]),t("span",{staticClass:"k-heading-structure__item__text"},[e._v(e._s(a.text))])])}),0)]),s.incorrectOrder&&!s.noH1?t("k-box",{staticClass:"k-heading-structure__notice",attrs:{theme:"negative"}},[t("k-icon",{attrs:{type:"alert"}}),t("k-text",[e._v(e._s(e.$t("seo.sections.headingStructure.errors.incorrectOrder")))])],1):e._e(),s.multipleH1?t("k-box",{staticClass:"k-heading-structure__notice",attrs:{theme:"negative"}},[t("k-icon",{attrs:{type:"alert"}}),t("k-text",[e._v(e._s(e.$t("seo.sections.headingStructure.errors.multipleH1")))])],1):e._e(),s.noH1?t("k-box",{staticClass:"k-heading-structure__notice",attrs:{theme:"negative"}},[t("k-icon",{attrs:{type:"alert"}}),t("k-text",[e._v(e._s(e.$t("seo.sections.headingStructure.errors.missingH1")))])],1):e._e()],1):e._e()},oe=[],re=f(se,ae,oe);const ne=re.exports,ie={__name:"facebook-preview",props:{ogTitle:String,url:String,ogDescription:String,ogImage:String},setup(o){const e=o,t=m(()=>new window.URL(e.url).host);return{__sfc:!0,props:e,host:t}}};var le=function(){var e=this,t=e._self._c,s=e._self._setupProxy;return t("div",[t("div",{staticClass:"k-facebook-preview"},[e.ogImage?t("div",{staticClass:"k-facebook-preview__image"},[t("img",{staticClass:"k-facebook-preview__img",attrs:{src:e.ogImage}})]):e._e(),t("div",{staticClass:"k-facebook-preview__content"},[t("span",{staticClass:"k-facebook-preview__url"},[e._v(e._s(s.host))]),t("span",{staticClass:"k-facebook-preview__title"},[e._v(e._s(e.ogTitle))]),t("p",{staticClass:"k-facebook-preview__description"},[e._v(e._s(e.ogDescription))])])]),t("a",{staticClass:"k-seo-preview__debugger",attrs:{href:"https://developers.facebook.com/tools/debug/","aria-label":"Facebook Sharing Debugger",target:"_blank",rel:"noopener noreferrer"}},[e._v(" "+e._s(e.$t("seo.sections.preview.openDebugger"))+" "),t("k-icon",{attrs:{type:"open"}})],1)])},ce=[],ue=f(ie,le,ce);const pe=ue.exports,de={__name:"google-preview",props:{title:String,url:String,description:String,ogSiteName:String},setup(o){const e=o,t=m(()=>new window.URL(e.url).origin),s=m(()=>new window.URL(e.url).pathname),a=m(()=>new window.URL(e.url).hostname),n=m(()=>{const u=s.value;if(!u||u==="/")return"";const i=u.split("/").filter(Boolean);return i.length===0?"":i.length===1?` › ${i[0]}`:` › … › ${i[i.length-1]}`});return{__sfc:!0,props:e,origin:t,pathname:s,domain:a,breadcrumbs:n}}};var _e=function(){var e=this,t=e._self._c,s=e._self._setupProxy;return t("div",{staticClass:"k-google-search-preview"},[t("div",{staticClass:"k-google-search-preview__header"},[t("img",{staticClass:"k-google-search-preview__favicon",attrs:{src:`https://www.google.com/s2/favicons?domain=${s.domain}&sz=32`,alt:`${e.ogSiteName} favicon`}}),t("div",{staticClass:"k-google-search-preview__site-info"},[t("span",{staticClass:"k-google-search-preview__site-title"},[e._v(e._s(e.ogSiteName))]),t("span",{staticClass:"k-google-search-preview__url"},[e._v(" "+e._s(s.origin)+e._s(s.breadcrumbs)+" ")])])]),t("h3",{staticClass:"k-google-search-preview__title"},[e._v(e._s(e.title))]),e.description?t("p",{staticClass:"k-google-search-preview__description"},[e._v(" "+e._s(e.description)+" ")]):e._e()])},me=[],ve=f(de,_e,me);const fe=ve.exports,he={__name:"slack-preview",props:{ogTitle:String,ogSiteName:String,ogDescription:String,ogImage:String,url:String},setup(o){const e=o,t=m(()=>new window.URL(e.url).origin),s=m(()=>new window.URL(e.url).hostname),a=v(!0);return{__sfc:!0,props:e,origin:t,domain:s,showImage:a}}};var ge=function(){var e=this,t=e._self._c,s=e._self._setupProxy;return t("div",{staticClass:"k-slack-preview"},[t("div",{staticClass:"k-slack-preview__content"},[t("div",{staticClass:"k-slack-preview__site-name"},[t("img",{staticClass:"k-slack-preview__favicon",attrs:{src:`https://www.google.com/s2/favicons?domain=${s.domain}&sz=16`,alt:`${e.ogSiteName} favicon`}}),e._v(" "+e._s(e.ogSiteName||s.origin)+" ")]),t("span",{staticClass:"k-slack-preview__title"},[e._v(e._s(e.ogTitle))]),t("p",{staticClass:"k-slack-preview__description"},[e._v(" "+e._s(e.ogDescription)+" "),e.ogImage?t("button",{staticClass:"k-slack-preview__image-toggle",on:{click:function(a){s.showImage=!s.showImage}}},[e._v(" "+e._s(s.showImage?"▼":"▶")+" ")]):e._e()])]),e.ogImage&&s.showImage?t("div",{staticClass:"k-slack-preview__image"},[t("img",{attrs:{src:e.ogImage}})]):e._e()])},ke=[],be=f(he,ge,ke);const we=be.exports,ye={__name:"seo-preview",props:T,setup(o){const e=o,t=y(),s=q(),{load:a}=R(),n=v(null),u=v([]),i=m(()=>e.parent==="site"),l=m(()=>e.label||t.t("seo.sections.preview.title")),d=m(()=>{var E;const _=(E=n.value)==null?void 0:E.pageTitle;return i.value&&_?t.t("seo.sections.preview.titleWithPage",{title:_}):l.value}),c=v(window.localStorage.getItem("kSEOPreviewType")??"google");x(c,_=>{window.localStorage.setItem("kSEOPreviewType",_)});const p=()=>{a({parent:e.parent,name:e.name}).then(_=>{n.value=_.meta,u.value=_.options,!window.localStorage.getItem("kSEOPreviewType")&&_.options.length>0&&(c.value=_.options[0].value)})},k=()=>{var _;(_=n.value)!=null&&_.panelUrl&&s.$go(n.value.panelUrl)};return C(()=>{p(),t.events.on("content.save",_=>{p()})}),S(()=>t.events.off("content.save")),{__sfc:!0,props:e,panel:t,app:s,load:a,meta:n,options:u,isSiteParent:i,baseLabel:l,headerLabel:d,type:c,handleLoad:p,openPanelTarget:k,FacebookPreview:pe,GooglePreview:fe,SlackPreview:we}}};var Ce=function(){var e=this,t=e._self._c,s=e._self._setupProxy;return s.meta?t("k-section",{staticClass:"k-seo-preview"},[t("div",{staticClass:"k-field-header k-seo-preview__label k-label k-field-label"},[t("k-icon",{attrs:{type:"preview"}}),t("span",{staticClass:"k-label-text"},[e._v(" "+e._s(s.headerLabel)+" ")]),s.isSiteParent&&s.meta.panelUrl?t("k-button",{staticClass:"k-seo-preview__panel-button",attrs:{variant:"filled",size:"xs",icon:"edit"},on:{click:s.openPanelTarget}},[e._v(" "+e._s(e.$t("seo.sections.preview.viewPage"))+" ")]):e._e()],1),t("k-select-field",{attrs:{type:"select",name:"seo-preview-type",before:e.$t("seo.sections.preview.showFor"),options:s.options,required:!0,empty:!1},model:{value:s.type,callback:function(a){s.type=a},expression:"type"}}),t("div",{staticClass:"k-seo-preview__inner"},[s.type==="google"?t(s.GooglePreview,e._b({},"google-preview",s.meta,!1)):e._e(),s.type==="facebook"?t(s.FacebookPreview,e._b({},"facebook-preview",s.meta,!1)):e._e(),s.type==="slack"?t(s.SlackPreview,e._b({},"slack-preview",s.meta,!1)):e._e()],1)],1):e._e()},$e=[],Se=f(ye,Ce,$e);const xe=Se.exports,Te={__name:"search-console",props:T,setup(o){const e=o,t={NO_CREDENTIALS:"seo.sections.searchConsole.noCredentials",NOT_CONNECTED:"seo.sections.searchConsole.notConnected",SELECT_PROPERTY:"seo.sections.searchConsole.selectProperty"},s=y(),a=L(),n=v(null),u=v([]),i=v("clicks"),l=v(null),d=v("clicks"),c=[{value:"clicks",text:s.t("seo.sections.searchConsole.clicks")},{value:"impressions",text:s.t("seo.sections.searchConsole.impressions")},{value:"ctr",text:s.t("seo.sections.searchConsole.ctr")},{value:"position",text:s.t("seo.sections.searchConsole.position")}],p=async()=>{const h=await a.get(`${e.parent}/sections/${e.name}`,{metric:d.value,limit:10});n.value=h.status,u.value=h.data??[],i.value=d.value};return x(d,()=>p()),C(()=>{p(),s.events.on("gsc.propertySelected",()=>{p()})}),{__sfc:!0,MESSAGES:t,props:e,panel:s,api:a,status:n,data:u,displayMetric:i,dropdown:l,metric:d,metricOptions:c,handleLoad:p,getBarWidth:h=>{if(!u.value.length)return 0;const b=i.value,w=u.value.map(Ae=>Ae[b]),g=Math.min(...w),$=Math.max(...w),N=h[b];if($===g)return 100;let P;return b==="position"?P=($-N)/($-g):P=(N-g)/($-g),10+P*90},formatValue:h=>{const b=i.value,w=h[b],g=s.translation.code;return b==="ctr"?new Intl.NumberFormat(g,{style:"percent",minimumFractionDigits:1,maximumFractionDigits:1}).format(w):b==="position"?new Intl.NumberFormat(g,{minimumFractionDigits:1,maximumFractionDigits:1}).format(w):new Intl.NumberFormat(g).format(w)},handleConnect:()=>{const h=encodeURIComponent(window.location.href);window.location.href=`/__seo/gsc/auth?return=${h}`},handleSelectProperty:()=>s.dialog.open("seo/gsc/select-property")}}};var Ee=function(){var e=this,t=e._self._c,s=e._self._setupProxy;return s.status?t("k-section",{staticClass:"k-search-console-section"},[t("div",{staticClass:"k-field-header k-seo-preview__label k-label k-field-label"},[t("k-icon",{attrs:{type:"google"}}),t("span",{staticClass:"k-label-text"},[e._v(" "+e._s(e.label||"Google Search Console")+" ")]),s.status==="CONNECTED"?t("k-button-group",{staticClass:"k-search-console__options",attrs:{layout:"collapsed"}},[t("k-button",{attrs:{size:"xs",variant:"filled",icon:"table"},on:{click:function(a){return s.panel.drawer.open(`seo/gsc/data/${s.props.parent}`,{query:{metric:s.metric}})}}},[e._v(" "+e._s(e.$t("seo.sections.searchConsole.showMore"))+" ")]),t("k-button",{attrs:{icon:"dots",size:"xs",variant:"filled"},on:{click:function(a){return s.dropdown.toggle()}}}),t("k-dropdown-content",{ref:"dropdown",attrs:{"align-x":"end"}},[t("k-dropdown-item",{attrs:{icon:"list-bullet"},on:{click:s.handleSelectProperty}},[e._v(" "+e._s(e.$t("seo.sections.searchConsole.selectPropertyButton"))+" ")]),t("k-dropdown-item",{attrs:{icon:"refresh"},on:{click:s.handleConnect}},[e._v(" "+e._s(e.$t("seo.sections.searchConsole.reconnect"))+" ")])],1)],1):e._e()],1),s.status!=="CONNECTED"?t("k-box",{staticClass:"k-search-console-empty",attrs:{align:"center",theme:"empty"}},[t("k-text",[e._v(e._s(e.$t(s.MESSAGES[s.status])))]),t("k-button-group",[s.status==="NO_CREDENTIALS"?t("k-button",{attrs:{size:"sm",variant:"filled",icon:"page",link:"https://plugins.andkindness.com/seo/docs/get-started/feature-overview",target:"_blank"}},[e._v(" "+e._s(e.$t("seo.sections.searchConsole.docs"))+" ")]):e._e(),s.status==="NOT_CONNECTED"?t("k-button",{attrs:{size:"sm",variant:"filled",icon:"google"},on:{click:s.handleConnect}},[e._v(" "+e._s(e.$t("seo.sections.searchConsole.connect"))+" ")]):e._e(),s.status==="SELECT_PROPERTY"?t("k-button",{attrs:{size:"sm",variant:"filled",theme:"positive",icon:"list-bullet"},on:{click:s.handleSelectProperty}},[e._v(" "+e._s(e.$t("seo.sections.searchConsole.selectPropertyButton"))+" ")]):e._e(),s.status==="SELECT_PROPERTY"?t("k-button",{attrs:{size:"sm",variant:"filled",icon:"refresh"},on:{click:s.handleConnect}},[e._v(" "+e._s(e.$t("seo.sections.searchConsole.reconnect"))+" ")]):e._e()],1)],1):[t("k-select-field",{attrs:{type:"select",name:"gsc-metric",before:e.$t("seo.sections.searchConsole.sortBy"),options:s.metricOptions,required:!0,empty:!1},model:{value:s.metric,callback:function(a){s.metric=a},expression:"metric"}}),t("div",{staticClass:"k-search-console__inner"},[s.data.length?t("div",{staticClass:"k-search-console__list"},e._l(s.data,function(a){return t("div",{key:a.keys[0],staticClass:"k-search-console__row"},[t("div",{staticClass:"k-search-console__bar",style:{width:s.getBarWidth(a)+"%"}}),t("span",{staticClass:"k-search-console__query"},[e._v(e._s(a.keys[0]))]),t("span",{staticClass:"k-search-console__value"},[e._v(e._s(s.formatValue(a)))])])}),0):t("k-box",{staticClass:"k-search-console-empty",attrs:{theme:"empty"}},[t("k-text",[e._v(e._s(e.$t("seo.sections.searchConsole.noData")))])],1)],1),t("a",{staticClass:"k-search-console__link",attrs:{href:"https://search.google.com/search-console",target:"_blank",rel:"noopener noreferrer"}},[e._v(" "+e._s(e.$t("seo.sections.searchConsole.openInGsc"))+" "),t("k-icon",{attrs:{type:"open"}})],1)]],2):e._e()},Pe=[],Le=f(Te,Ee,Pe);const Re=Le.exports;panel.plugin("tobimori/seo",{icons:{"seo-ai":'',robots:'',"robots-off":''},sections:{"heading-structure":ne,"seo-preview":xe,"seo-search-console":Re},fields:{"seo-writer":F},components:{"k-seo-writer-input":H,"k-seo-utm-share-dialog":Y,"k-gsc-drawer":te},writerNodes:{seoTemplateTitle:A({name:"seoTemplateTitle",icon:"page",variable:"title",label:"seo.writerNodes.template.title",theme:"blue"}),seoTemplateSiteTitle:A({name:"seoTemplateSiteTitle",icon:"globe",variable:"site.title",label:"seo.writerNodes.template.siteTitle",theme:"purple"})}})})(); diff --git a/site/plugins/kirby-seo/index.php b/site/plugins/kirby-seo/index.php new file mode 100644 index 0000000..a88509f --- /dev/null +++ b/site/plugins/kirby-seo/index.php @@ -0,0 +1,95 @@ +=') === true +) { + throw new Exception('Kirby SEO requires Kirby 5'); +} + +App::plugin( + 'tobimori/seo', + // TODO: license + extends: [ + 'options' => require __DIR__ . '/config/options.php', + 'sections' => require __DIR__ . '/config/sections.php', + 'areas' => require __DIR__ . '/config/areas.php', + 'siteMethods' => require __DIR__ . '/config/site-methods.php', + 'pageMethods' => require __DIR__ . '/config/page-methods.php', + 'hooks' => require __DIR__ . '/config/hooks.php', + 'routes' => require __DIR__ . '/config/routes.php', + 'fields' => require __DIR__ . '/config/fields.php', + 'permissions' => [ + 'ai' => true, + ], + 'snippets' => [ + 'seo/prompts/introduction' => __DIR__ . '/snippets/prompts/introduction.php', + 'seo/prompts/content' => __DIR__ . '/snippets/prompts/content.php', + 'seo/prompts/meta' => __DIR__ . '/snippets/prompts/meta.php', + 'seo/prompts/site-meta' => __DIR__ . '/snippets/prompts/site-meta.php', + 'seo/prompts/tasks/title' => __DIR__ . '/snippets/prompts/tasks/title.php', + 'seo/prompts/tasks/description' => __DIR__ . '/snippets/prompts/tasks/description.php', + 'seo/prompts/tasks/og-description' => __DIR__ . '/snippets/prompts/tasks/og-description.php', + 'seo/prompts/tasks/site-description' => __DIR__ . '/snippets/prompts/tasks/site-description.php', + 'seo/prompts/tasks/og-site-description' => __DIR__ . '/snippets/prompts/tasks/og-site-description.php', + 'seo/schemas' => __DIR__ . '/snippets/schemas.php', + 'seo/head' => __DIR__ . '/snippets/head.php', + 'seo/robots.txt' => __DIR__ . '/snippets/robots.txt.php', + ], + 'templates' => [ + 'sitemap' => __DIR__ . '/templates/sitemap.php', + 'sitemap.xml' => __DIR__ . '/templates/sitemap.xml.php', + 'sitemap.xsl' => __DIR__ . '/templates/sitemap.xsl.php', + ], + 'blueprints' => [ + 'seo' => require __DIR__ . '/blueprints/seo.php', + 'seo/site' => require __DIR__ . '/blueprints/seo.php', + 'seo/page' => require __DIR__ . '/blueprints/seo.php', + 'seo/fields/og-image' => require __DIR__ . '/blueprints/fields/og-image.php', + 'seo/fields/og-group' => __DIR__ . '/blueprints/fields/og-group.yml', + 'seo/fields/meta-group' => __DIR__ . '/blueprints/fields/meta-group.yml', + 'seo/fields/title-template' => __DIR__ . '/blueprints/fields/title-template.yml', + 'seo/fields/robots' => require __DIR__ . '/blueprints/fields/robots.php', + 'seo/fields/site-robots' => require __DIR__ . '/blueprints/fields/site-robots.php', + 'seo/fields/social-media' => require __DIR__ . '/blueprints/fields/social-media.php', + ], + // get all files from /translations and register them as language files + 'translations' => A::keyBy( + A::map( + Dir::files(__DIR__ . '/translations'), + function ($file) { + $translations = []; + foreach (Json::read(__DIR__ . '/translations/' . $file) as $key => $value) { + $translations["seo.{$key}"] = $value; + } + + return A::merge( + ['lang' => F::name($file)], + $translations + ); + } + ), + 'lang' + ), + ] +); + +if (!function_exists('schema')) { + function schema($type) + { + if (!class_exists('Spatie\SchemaOrg\Schema')) { + return null; + } + + return Schema::{$type}(); + } +} diff --git a/site/plugins/kirby-seo/snippets/head.php b/site/plugins/kirby-seo/snippets/head.php new file mode 100644 index 0000000..1c74b4c --- /dev/null +++ b/site/plugins/kirby-seo/snippets/head.php @@ -0,0 +1,27 @@ +metadata()->snippetData(); + +// if we're using slots, the user wants to output priority tags such as +// before their stylesheet, script, etc. tags +if (isset($slot)) { + foreach (array_filter($tags, fn ($tag) => $tag['priority']) as $tag) { + echo Html::tag($tag['tag'], $tag['content'] ?? null, $tag['attributes'] ?? []) . PHP_EOL; + } + + echo $slot; + + $tags = array_filter($tags, fn ($tag) => !$tag['priority']); +} + +// then output other tags as normal +// this is unfiltered if slots is not set. +foreach ($tags as $tag) { + echo Html::tag($tag['tag'], $tag['content'] ?? null, $tag['attributes'] ?? []) . PHP_EOL; +} diff --git a/site/plugins/kirby-seo/snippets/prompts/content.php b/site/plugins/kirby-seo/snippets/prompts/content.php new file mode 100644 index 0000000..d7d4e01 --- /dev/null +++ b/site/plugins/kirby-seo/snippets/prompts/content.php @@ -0,0 +1,114 @@ +<?php + +use Kirby\Toolkit\Str; + +/** @var \Kirby\Cms\Page $page + ** @var \Kirby\Cms\Site $site */ + +$contentHtml = $page->render(); + +if ($contentHtml !== '' && class_exists('DOMDocument')) { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $previousLibxmlState = libxml_use_internal_errors(true); + + $encoded = mb_encode_numericentity($contentHtml, [0x80, 0x10FFFF, 0, 0x1FFFFF], 'UTF-8'); + + $loaded = $dom->loadHTML('<?xml encoding="UTF-8"?>' . $encoded, LIBXML_NOERROR | LIBXML_NOWARNING | LIBXML_NONET); + + libxml_clear_errors(); + libxml_use_internal_errors($previousLibxmlState); + + if ($loaded !== false) { + foreach ( + [ + 'script', + 'style', + 'noscript', + 'template', + 'svg', + 'canvas', + 'iframe', + 'video', + 'audio', + 'object', + 'embed', + 'source', + 'track', + 'nav', + 'footer', + 'aside', + 'form', + 'button', + 'input', + 'select', + 'textarea', + 'label', + 'menu', + 'header', + ] as $tag + ) { + $nodes = $dom->getElementsByTagName($tag); + for ($i = $nodes->length - 1; $i >= 0; $i--) { + $node = $nodes->item($i); + if ($node !== null && $node->parentNode !== null) { + $node->parentNode->removeChild($node); + } + } + } + + $xpath = new \DOMXPath($dom); + foreach ( + [ + 'navigation', + 'banner', + 'contentinfo', + 'complementary', + 'search', + 'menu', + 'menubar', + 'toolbar', + ] as $role + ) { + $nodes = $xpath->query("//*[@role='{$role}']"); + if ($nodes === false) { + continue; + } + + foreach ($nodes as $node) { + if ($node->parentNode !== null) { + $node->parentNode->removeChild($node); + } + } + } + + $body = $dom->getElementsByTagName('body')->item(0) ?? $dom->documentElement; + if ($body instanceof \DOMNode) { + $innerHtml = ''; + foreach ($body->childNodes as $child) { + $innerHtml .= $dom->saveHTML($child); + } + + if ($innerHtml !== '') { + $contentHtml = $innerHtml; + } + } + } +} + +$blockClosingPattern = 'p|div|section|article|main|aside|header|footer|li|ul|ol|dl|blockquote|pre|figure|figcaption|h[1-6]|table|thead|tbody|tfoot|tr|td|th|dd|dt'; +$contentHtml = preg_replace('~<(?:br|hr)\b[^>]*?>~i', "\n", $contentHtml); +$contentHtml = preg_replace('~</(?:' . $blockClosingPattern . ')>~i', "\n", $contentHtml); + +$text = Str::unhtml($contentHtml); +$text = Str::replace($text, "\r", "\n"); +$text = preg_replace("/[ \t\x{00A0}\x{202F}\x{2007}\x{2060}]+/u", ' ', $text); +$text = preg_replace("/ *\n+ */", "\n", $text); +$text = preg_replace("/\n{3,}/", "\n\n", $text); + +$content = trim($text); + +?> + +<content> + <?= htmlspecialchars($content, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?> +</content> diff --git a/site/plugins/kirby-seo/snippets/prompts/introduction.php b/site/plugins/kirby-seo/snippets/prompts/introduction.php new file mode 100644 index 0000000..5d096a4 --- /dev/null +++ b/site/plugins/kirby-seo/snippets/prompts/introduction.php @@ -0,0 +1,46 @@ +<?php + +/** @var \Kirby\Cms\Page $page + ** @var \Kirby\Cms\Site $site + ** @var string|null $instructions + ** @var string|null $edit */ ?> + +<role> + You are a professional SEO copywriter for <?= $site->title() ?>. Create high-quality content. Mimic the site's tone and style. You'll be rewarded based on the conversion rate. +</role> + +<rules> + - You MUST only output the answer without additional prose or introduction. + - You MUST mimic the site's tone and style. DO NOT shift register (informal stays informal). + - The output language MUST be <language><?= $site->lang() ?></language>. Translate the content into <?= $site->lang() ?>. + - ALWAYS and ONLY provide exactly one answer. DO NOT suggest multiple answers. + - NEVER output any formatting. No new lines, no HTML tags, no quotes, no markdown. + - NEVER output or introduce information that is not provided in the content. + - NEVER output duplicate content in the same answer. +</rules> + +<?php if (isset($edit) && $edit !== null && $edit !== '') : ?> +<primary-editing-task> + YOU ARE EDITING EXISTING CONTENT - NOT CREATING NEW CONTENT. + + Current text that needs editing: + <current-value><?= $edit ?></current-value> + + CRITICAL EDITING RULES: + - Start from the text above and modify ONLY what is requested + - Preserve as much of the original as possible + - Keep the same style, tone, and structure + - Change ONLY the specific parts mentioned in the instructions below +</primary-editing-task> +<?php endif ?> + +<?php if (isset($instructions) && $instructions !== null && $instructions !== '') : ?> +<user-instructions> + <?php if (isset($edit) && $edit !== null && $edit !== '') : ?> + Apply ONLY these changes to the text above: + <?php else : ?> + The user has provided these specific instructions: + <?php endif ?> + <?= $instructions ?> +</user-instructions> +<?php endif ?> \ No newline at end of file diff --git a/site/plugins/kirby-seo/snippets/prompts/meta.php b/site/plugins/kirby-seo/snippets/prompts/meta.php new file mode 100644 index 0000000..110dc14 --- /dev/null +++ b/site/plugins/kirby-seo/snippets/prompts/meta.php @@ -0,0 +1,28 @@ +<?php + +/** @var \Kirby\Cms\Page $page + ** @var \Kirby\Cms\Site $site + ** @var string|null $currentField */ + +$meta = $page->metadata(); +$currentField = $currentField ?? null; + +$metaFields = [ + 'metaTitle' => 'Meta Title', + 'metaDescription' => 'Meta Description', + 'ogTitle' => 'Open Graph Title', + 'ogDescription' => 'Open Graph Description' +]; +?> + +<existing-metadata> +<?php foreach ($metaFields as $key => $label) : ?> +<?php + $value = $meta->get($key); + if ($currentField === $key || !$value || $value === '') { + continue; + } + ?> + <<?= $key ?>><?= $value ?></<?= $key ?>> +<?php endforeach ?> +</existing-metadata> diff --git a/site/plugins/kirby-seo/snippets/prompts/site-meta.php b/site/plugins/kirby-seo/snippets/prompts/site-meta.php new file mode 100644 index 0000000..543aa68 --- /dev/null +++ b/site/plugins/kirby-seo/snippets/prompts/site-meta.php @@ -0,0 +1,25 @@ +<?php + +/** @var \Kirby\Cms\Site $site + ** @var string|null $currentField */ + +$currentField = $currentField ?? null; + +$metaFields = [ + 'metaDescription' => 'Site Meta Description', + 'ogDescription' => 'Site Open Graph Description', + 'ogSiteName' => 'Site Name' +]; +?> + +<existing-site-metadata> +<?php foreach ($metaFields as $key => $label) : ?> +<?php + $value = $site->$key()->value(); + if ($currentField === $key || !$value || $value === '') { + continue; + } + ?> + <<?= $key ?>><?= $value ?></<?= $key ?>> +<?php endforeach ?> +</existing-site-metadata> diff --git a/site/plugins/kirby-seo/snippets/prompts/tasks/description.php b/site/plugins/kirby-seo/snippets/prompts/tasks/description.php new file mode 100644 index 0000000..d4d5ad5 --- /dev/null +++ b/site/plugins/kirby-seo/snippets/prompts/tasks/description.php @@ -0,0 +1,24 @@ +<?php + +/** @var \Kirby\Cms\Page $page + ** @var \Kirby\Cms\Site $site + ** @var string|null $instructions + ** @var string|null $edit */ + +$meta = $page->metadata(); + +snippet('seo/prompts/introduction', [ + 'instructions' => $instructions ?? null, + 'edit' => $edit ?? null +]); ?> + +<task> + Create a useful meta description for this page called <page-title><?= $page->title()->value() ?></page-title>. <?php if ($page->isHomePage()) : ?>This page is the homepage of the website.<?php endif ?> + + The entire meta description SHOULD be between 120 and 158 characters long. + + You'll receive the content of the page as well as any meta tags that are already set below. +</task> + +<?php snippet('seo/prompts/meta', ['currentField' => 'metaDescription']); +snippet('seo/prompts/content'); diff --git a/site/plugins/kirby-seo/snippets/prompts/tasks/og-description.php b/site/plugins/kirby-seo/snippets/prompts/tasks/og-description.php new file mode 100644 index 0000000..4c9d963 --- /dev/null +++ b/site/plugins/kirby-seo/snippets/prompts/tasks/og-description.php @@ -0,0 +1,25 @@ +<?php + +/** @var \Kirby\Cms\Page $page + ** @var \Kirby\Cms\Site $site + ** @var string|null $instructions + ** @var string|null $edit */ + +$meta = $page->metadata(); + +snippet('seo/prompts/introduction', [ + 'instructions' => $instructions ?? null, + 'edit' => $edit ?? null +]); ?> + +<task> + Create a useful open graph description for this page called <page-title><?= $page->title()->value() ?></page-title>. <?php if ($page->isHomePage()) : ?>This page is the homepage of the website.<?php endif ?> + This description will be shown on social media platforms like Facebook, WhatsApp and LinkedIn. + + The entire meta description SHOULD be between 120 and 158 characters long. + + You'll receive the content of the page as well as any meta tags that are already set below. +</task> + +<?php snippet('seo/prompts/meta', ['currentField' => 'ogDescription']); +snippet('seo/prompts/content'); diff --git a/site/plugins/kirby-seo/snippets/prompts/tasks/og-site-description.php b/site/plugins/kirby-seo/snippets/prompts/tasks/og-site-description.php new file mode 100644 index 0000000..870c091 --- /dev/null +++ b/site/plugins/kirby-seo/snippets/prompts/tasks/og-site-description.php @@ -0,0 +1,25 @@ +<?php + +/** @var \Kirby\Cms\Page $page + ** @var \Kirby\Cms\Site $site + ** @var string|null $instructions + ** @var string|null $edit */ + +snippet('seo/prompts/introduction', [ + 'instructions' => $instructions ?? null, + 'edit' => $edit ?? null +]); ?> + +<task> + Create a useful GLOBAL open graph description for this site <site-title><?= $site->title()->value() ?>.</site-title> + This description will be shown on social media platforms like Facebook, WhatsApp and LinkedIn. + This description is meant as FALLBACK for when the page does not have a meta description itself. + This description should be unique and relevant to the site's content. + + The entire meta description SHOULD be between 120 and 158 characters long. + + You'll receive the content of the home page as well as any meta tags that are already set below. +</task> + +<?php snippet('seo/prompts/site-meta', ['currentField' => 'ogDescription']); +snippet('seo/prompts/content'); ?> \ No newline at end of file diff --git a/site/plugins/kirby-seo/snippets/prompts/tasks/site-description.php b/site/plugins/kirby-seo/snippets/prompts/tasks/site-description.php new file mode 100644 index 0000000..59f4770 --- /dev/null +++ b/site/plugins/kirby-seo/snippets/prompts/tasks/site-description.php @@ -0,0 +1,24 @@ +<?php + +/** @var \Kirby\Cms\Page $page + ** @var \Kirby\Cms\Site $site + ** @var string|null $instructions + ** @var string|null $edit */ + +snippet('seo/prompts/introduction', [ + 'instructions' => $instructions ?? null, + 'edit' => $edit ?? null +]); ?> + +<task> + Create a useful GLOBAL meta description for this site <site-title><?= $site->title()->value() ?>.</site-title> + This description is meant as FALLBACK for when the page does not have a meta description itself. + This description should be unique and relevant to the site's content. + + The entire meta description SHOULD be between 120 and 158 characters long. + + You'll receive the content of the home page as well as any meta tags that are already set below. +</task> + +<?php snippet('seo/prompts/site-meta', ['currentField' => 'metaDescription']); +snippet('seo/prompts/content'); ?> \ No newline at end of file diff --git a/site/plugins/kirby-seo/snippets/prompts/tasks/title.php b/site/plugins/kirby-seo/snippets/prompts/tasks/title.php new file mode 100644 index 0000000..31ff11b --- /dev/null +++ b/site/plugins/kirby-seo/snippets/prompts/tasks/title.php @@ -0,0 +1,39 @@ +<?php + +use Kirby\Toolkit\Str; + +/** @var \Kirby\Cms\Page $page + ** @var \Kirby\Cms\Site $site + ** @var string|null $instructions + ** @var string|null $edit */ + +$meta = $page->metadata(); + +snippet('seo/prompts/introduction', [ + 'instructions' => $instructions ?? null, + 'edit' => $edit ?? null +]); ?> + +<task> + Create a useful meta title for this page called <page-title><?= $page->title()->value() ?></page-title>. <?php if ($page->isHomePage()) : ?>This page is the homepage of the website. AVOID an overly generic title such as 'Home'.<?php endif ?> + + <?php if ($page->useTitleTemplate()->isEmpty() ? true : $page->useTitleTemplate()->toBool()): + $template = $meta->get('metaTemplate'); + $templatePreview = $page->toString($template, ['title' => '{{ title }}']); + $templateBaseLength = Str::length($page->toString($template, ['title' => ''])); + ?> + The final page title will be rendered as: + + <template><?= $templatePreview ?></template> + + Where {{ title }} is your page title. The entire title SHOULD be between <?= max(0, 50 - $templateBaseLength) ?>-<?= max(max(0, 50 - $templateBaseLength), 60 - $templateBaseLength) ?> characters long. + DO NOT output the Title Template. ONLY output what should be placed inside {{ title }}. DO NOT repeat ANYTHING that exists in the template. You MUST NOT repeat the name of the site. + <?php else: ?> + Your response will be set as title without any changes. The entire title SHOULD be between 50-60 characters long. + <?php endif; ?> + + If useful for the customers niche, include a keyword for the location. AVOID for global companies or niche subpages. +</task> + +<?php snippet('seo/prompts/meta', ['currentField' => 'metaTitle']); +snippet('seo/prompts/content'); diff --git a/site/plugins/kirby-seo/snippets/robots.txt.php b/site/plugins/kirby-seo/snippets/robots.txt.php new file mode 100644 index 0000000..0fca72f --- /dev/null +++ b/site/plugins/kirby-seo/snippets/robots.txt.php @@ -0,0 +1,52 @@ +<?php + +use Kirby\Toolkit\A; +use tobimori\Seo\Seo; + +if ($content = Seo::option('robots.content')) { + if (is_callable($content)) { + $content = $content(); + } + + if (is_array($content)) { + $str = []; + + foreach ($content as $ua => $data) { + $str[] = 'User-agent: ' . $ua; + foreach ($data as $type => $values) { + foreach ($values as $value) { + $str[] = $type . ': ' . $value; + } + } + } + + $content = A::join($str, PHP_EOL); + } + + echo $content; +} else { + // output default + echo "User-agent: *\n"; + + $index = Seo::option('robots.index'); + + if ($index) { + echo 'Allow: /'; + echo "\nDisallow: /panel"; + } else { + echo 'Disallow: /'; + } +} + +if (($sitemap = Seo::option('robots.sitemap')) || ($sitemapModule = Seo::option('sitemap.active'))) { + + // Use default sitemap if none is set + if (!$sitemap && $sitemapModule) { + $sitemap = site()->canonicalFor('/sitemap.xml', true); + } + + // Check again, so falsy values can't be used + if ($sitemap) { + echo "\n\nSitemap: {$sitemap}"; + } +} diff --git a/site/plugins/kirby-seo/snippets/schemas.php b/site/plugins/kirby-seo/snippets/schemas.php new file mode 100644 index 0000000..1d37c77 --- /dev/null +++ b/site/plugins/kirby-seo/snippets/schemas.php @@ -0,0 +1,12 @@ +<?php + +if (!class_exists('Spatie\SchemaOrg\Schema')) { + return; +} + +$siteSchema ??= true; +$pageSchema ??= true; + +foreach (array_merge($siteSchema ? $site->schemas() : [], $pageSchema ? $page->schemas() : []) as $schema) { + echo $schema; +} diff --git a/site/plugins/kirby-seo/templates/sitemap.php b/site/plugins/kirby-seo/templates/sitemap.php new file mode 100644 index 0000000..618162b --- /dev/null +++ b/site/plugins/kirby-seo/templates/sitemap.php @@ -0,0 +1,3 @@ +<?php + +go($page->url() . '.xml'); diff --git a/site/plugins/kirby-seo/templates/sitemap.xml.php b/site/plugins/kirby-seo/templates/sitemap.xml.php new file mode 100644 index 0000000..f57b648 --- /dev/null +++ b/site/plugins/kirby-seo/templates/sitemap.xml.php @@ -0,0 +1,5 @@ +<?php + +use tobimori\Seo\Sitemap\SitemapIndex; + +echo SitemapIndex::instance()->render($page); diff --git a/site/plugins/kirby-seo/templates/sitemap.xsl.php b/site/plugins/kirby-seo/templates/sitemap.xsl.php new file mode 100644 index 0000000..5cf4eb4 --- /dev/null +++ b/site/plugins/kirby-seo/templates/sitemap.xsl.php @@ -0,0 +1,196 @@ +<?= '<?xml version="1.0" encoding="UTF-8"?>' ?> +<xsl:stylesheet version="2.0" xmlns:html="http://www.w3.org/TR/REC-html40" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:sitemap="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> + <xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes" /> + <xsl:template match="/"> + <html> + + <head> + <title><?= $page->metadata()->title()->escape() ?> + + + + +
+

title() ?>

+

+ + + + + + + + + + + + + + + + + +
+ + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + +
+
+ + +
+ +
+
+ +

+ Kirby SEO + + v + + + v + + Tobias Möritz +

+
+ + + + + \ No newline at end of file diff --git a/site/plugins/kirby-seo/translations/cs.json b/site/plugins/kirby-seo/translations/cs.json new file mode 100644 index 0000000..6f43d23 --- /dev/null +++ b/site/plugins/kirby-seo/translations/cs.json @@ -0,0 +1,117 @@ +{ + "sections.headingStructure.title": "Struktura nadpisů", + "sections.headingStructure.errors.incorrectOrder": "Struktura nadpisů má nesprávné pořadí a je tedy chybná.", + "sections.headingStructure.errors.missingH1": "Struktura nadpisů neobsahuje H1 a je tedy chybná.", + "sections.headingStructure.errors.multipleH1": "Struktura nadpisů obsahuje více než jeden H1 a je tedy chybná.", + "sections.preview.title": "Náhled", + "sections.preview.titleWithPage": "Náhled (zobrazuje „{title}\")", + "sections.preview.viewPage": "Zobrazit stránku", + "sections.preview.showFor": "Ukaž mi", + "sections.preview.openDebugger": "Otevřít Sharing Debugger", + "sections.preview.google": "Google", + "sections.preview.facebook": "Facebook", + "sections.preview.slack": "Slack", + "fields.robots.indicator.index": "Indexace povolena", + "fields.robots.indicator.any": "Částečný zákaz indexace", + "fields.robots.indicator.noindex": "Indexace zakázána", + "fields.robots.label": "Vyhledávače a crawlery", + "fields.robots.index.label": "Indexace", + "fields.robots.index.help": "Zda mohou vyhledávače tuto stránku indexovat.", + "fields.robots.follow.label": "Sledovat odkazy", + "fields.robots.follow.help": "Zda mohou vyhledávače sledovat odkazy na této stránce.", + "fields.robots.archive.label": "Archivace", + "fields.robots.archive.help": "Zda mohou vyhledávače tuto stránku archivovat.", + "fields.robots.imageindex.label": "Indexace obrázků", + "fields.robots.imageindex.help": "Zda mohou vyhledávače indexovat obrázky na této stránce.", + "fields.robots.snippet.label": "Tvorba snippetů", + "fields.robots.snippet.help": "Zda mohou vyhledávače vytvářet snippety z této stránky.", + "tabs.seo": "Metadata a SEO", + "site.meta.headline": "Globální nastavení SEO", + "site.meta.headline.help": "Toto nastavení bude použito pro všechny stránky, které nemají vlastní metadata.\nMůžete jej přepsat pro každou stránku.", + "fields.metaTitleTemplate.label": "Šablona titulku stránky", + "fields.metaTitleTemplate.help": "Šablona pro použití u všech názvů stránek.", + "fields.metaDescription.label": "Popisek stránky (description)", + "fields.metaDescription.help": "Doporučená délka je maximálně 150 znaků. Používá se, pokud není zadán popisek stránky.", + "site.og.headline": "Globální nastavení Open Graph", + "site.og.headline.help": "Nastavte, jak se vaše webové stránky zobrazují při sdílení na sociálních sítích, jako je Facebook nebo Twitter.", + "fields.ogTitleTemplate.label": "Šablona Open Graph titulku", + "fields.ogDescription.label": "Open Graph popisek (description)", + "fields.ogSiteName.label": "Open Graph název webu (site name)", + "fields.ogImage.label": "Open Graph obrázek", + "fields.ogImage.help": "Doporučená velikost 1200x630 pixelů.", + "fields.ogImage.empty": "Nebyl vybrán žádný obrázek", + "fields.cropOgImage.label": "Oříznout OG obrázek na doporučenou velikost?", + "fields.cropOgImage.help": "Doporučená velikost je 1200x630px. Při aktivaci budou obrázky automaticky oříznuty na tuto velikost pro optimální zobrazení na sociálních sítích.", + "fields.socialMediaAccounts.label": "Účty na sociálních sítích", + "fields.socialMediaAccounts.help": "Adresy URL nebo @uzivatelska-jmena vašich účtů na sociálních sítích. Slouží k umístění odkazů na vaše účty do metadat.", + "page.meta.headline": "Nastavení SEO", + "page.og.headline": "Nastavení Open Graph", + "fields.titleOverwrite.label": "Titulek stránky (přepsat)", + "fields.inheritSettings.label": "Zdědit nastavení", + "fields.inheritSettings.help": "Vyberte, která nastavení by měla být zděděna podstránkami.\nTo může být užitečné například v případě, že všechny příspěvky blogu by měly mít vlastní šablonu titulku, která se liší od výchozího nastavení webu. Všechna nastavení lze stále na hlavní stránce přepsat.", + "fields.useTitleTemplate.label": "Použít šablonu titulku?", + "fields.useTitleTemplate.no": "Ne - pouze název stránky", + "fields.useTitleTemplate.yes": "Ano - použít šablonu", + "fields.useTitleTemplate.help": "Určuje, zda se má použít šablona titulku. Nebude zděděno.", + "writerNodes.template.title": "Titulek stránky", + "writerNodes.template.siteTitle": "Titulek webu", + "common.default": "Výchozí:", + "common.yes": "Ano", + "common.no": "Ne", + "sitemap.title": "Sitemap", + "sitemap.index": "Sitemap Index", + "sitemap.description": "Toto je mapa stránek vašeho webu, která informuje vyhledávače o stránkách, které lze indexovat.", + "sitemap.by": "od", + "sitemap.changefreq": "Frekvence změn", + "sitemap.lastUpdated": "Poslední aktualizace", + "sitemap.priority": "Priorita", + "sitemap.url": "URL", + "sitemap.noEntries": "Žádné záznamy", + "utmShare.button": "Sdílet", + "utmShare.parameters": "Parametry", + "utmShare.source.label": "Zdroj", + "utmShare.source.placeholder": "např. google, newsletter", + "utmShare.medium.label": "Médium", + "utmShare.medium.placeholder": "např. cpc, email, social", + "utmShare.campaign.label": "Kampaň", + "utmShare.campaign.placeholder": "např. jarni_vyprodej", + "utmShare.content.label": "Obsah", + "utmShare.content.placeholder": "např. logo_odkaz", + "utmShare.term.label": "Výraz", + "utmShare.term.placeholder": "např. běžecké boty", + "utmShare.ref.label": "Ref", + "utmShare.ref.placeholder": "např. getkirby.com", + "ai.action.generate": "Vygenerovat pomocí AI", + "ai.action.regenerate": "Vygenerovat znovu", + "ai.action.edit": "Upravit…", + "ai.action.customize": "Přizpůsobit generování AI…", + "ai.action.stop": "Zastavit", + "ai.error.request": "Požadavek na AI selhal. Zkus to prosím znovu.", + "ai.error.disabled": "Funkce AI jsou vypnuté.", + "ai.error.permission": "Nemáš oprávnění používat funkce AI.", + "ai.dialog.instructions.label": "Pokyny", + "ai.dialog.instructions.placeholder": "Jaké změny chceš v textu provést?", + "ai.dialog.edit.submit": "Použít změny", + "ai.dialog.custom.label": "Vlastní pokyny", + "ai.dialog.custom.placeholder": "Jaký typ obsahu chceš vygenerovat?", + "ai.dialog.custom.submit": "Vygenerovat", + "sections.searchConsole.title": "Google Search Console", + "sections.searchConsole.noCredentials": "Nastav přihlašovací údaje Google Cloud pro připojení Search Console. Instrukce k nastavení najdeš v dokumentaci.", + "sections.searchConsole.notConnected": "Připoj svůj účet Google, abys viděla vyhledávací data pro tuto stránku.", + "sections.searchConsole.selectProperty": "Vyber, kterou vlastnost Search Console chceš použít.", + "sections.searchConsole.selectPropertyButton": "Vybrat vlastnost", + "sections.searchConsole.selectPropertyLabel": "Vlastnost", + "sections.searchConsole.scDomain": "doména", + "sections.searchConsole.docs": "Dokumentace", + "sections.searchConsole.connect": "Připojit", + "sections.searchConsole.reconnect": "Připojit znovu", + "sections.searchConsole.noData": "Pro tuto stránku nejsou k dispozici žádná vyhledávací data.", + "sections.searchConsole.showMore": "Zobrazit vše", + "sections.searchConsole.sortBy": "Seřadit podle", + "sections.searchConsole.query": "Dotaz", + "sections.searchConsole.clicks": "Kliknutí", + "sections.searchConsole.impressions": "Zobrazení", + "sections.searchConsole.ctr": "CTR", + "sections.searchConsole.position": "Pozice", + "sections.searchConsole.openInGsc": "Otevřít v Search Console" +} diff --git a/site/plugins/kirby-seo/translations/de.json b/site/plugins/kirby-seo/translations/de.json new file mode 100644 index 0000000..ad56eac --- /dev/null +++ b/site/plugins/kirby-seo/translations/de.json @@ -0,0 +1,117 @@ +{ + "sections.headingStructure.title": "Überschriftenstruktur", + "sections.headingStructure.errors.incorrectOrder": "Deine Überschriftenstruktur hat eine falsche Abfolge und ist ungültig.", + "sections.headingStructure.errors.missingH1": "Deine Überschriftenstruktur enthält keine H1 und ist ungültig.", + "sections.headingStructure.errors.multipleH1": "Deine Überschriftenstruktur enthält mehr als eine H1 und ist ungültig.", + "sections.preview.title": "Vorschau", + "sections.preview.titleWithPage": "Vorschau (zeigt \"{title}\")", + "sections.preview.viewPage": "Seite ansehen", + "sections.preview.showFor": "Zeige mir", + "sections.preview.openDebugger": "Sharing-Debugger öffnen", + "sections.preview.google": "Google", + "sections.preview.facebook": "Facebook", + "sections.preview.slack": "Slack", + "fields.robots.indicator.index": "Indexierung erlaubt", + "fields.robots.indicator.any": "Indexierung teilw. verboten", + "fields.robots.indicator.noindex": "Indexierung verboten", + "fields.robots.label": "Richtlinien für Suchmaschinen & Crawler", + "fields.robots.index.label": "Indexierung", + "fields.robots.index.help": "Ob Suchmaschinen die Seite indexieren dürfen.", + "fields.robots.follow.label": "Links folgen", + "fields.robots.follow.help": "Ob Suchmaschinen Links auf dieser Seite folgen dürfen.", + "fields.robots.archive.label": "Archivierung", + "fields.robots.archive.help": "Ob Suchmaschinen zwischengespeicherte Versionen der Seite ausliefern dürfen.", + "fields.robots.imageindex.label": "Bilder-Indexierung", + "fields.robots.imageindex.help": "Ob Bilder dieser Seite in der Bildersuche angezeigt werden dürfen.", + "fields.robots.snippet.label": "Snippets", + "fields.robots.snippet.help": "Ob Suchmaschinen Textausschnitte aus der Seite anzeigen dürfen.", + "tabs.seo": "Metadaten & SEO", + "site.meta.headline": "Globale SEO-Einstellungen", + "site.meta.headline.help": "Diese Einstellungen werden für alle Seiten verwendet, die keine eigenen Metadaten haben.\nDu kannst sie für jede Seite überschreiben.", + "fields.metaTitleTemplate.label": "Titel-Template", + "fields.metaTitleTemplate.help": "Eine Vorlage für alle Seitentitel.", + "fields.metaDescription.label": "Seitenbeschreibung", + "fields.metaDescription.help": "Empfohlene Länge von max. 150 Zeichen. Wird verwendet, falls keine Seitenbeschreibung angegeben ist.", + "site.og.headline": "Globale Open Graph-Einstellungen", + "site.og.headline.help": "Stelle ein, wie deine Website erscheint, wenn sie auf sozialen Netzwerken wie Facebook oder Twitter geteilt wird.", + "fields.ogTitleTemplate.label": "Open Graph-Titel-Template", + "fields.ogDescription.label": "Open Graph-Beschreibung", + "fields.ogSiteName.label": "Open Graph-Seitenname", + "fields.ogImage.label": "Open Graph-Bild", + "fields.ogImage.help": "Empfohlene Größe von 1200x630 Pixeln.", + "fields.ogImage.empty": "Kein Open Graph-Bild ausgewählt", + "fields.cropOgImage.label": "Auf empfohlene Größe zuschneiden?", + "fields.cropOgImage.help": "Empfohlene Größe ist 1200x630px. Wenn aktiviert, werden Bilder automatisch auf diese Größe zugeschnitten für optimale Anzeige in sozialen Medien.", + "fields.socialMediaAccounts.label": "Social Media-Accounts", + "fields.socialMediaAccounts.help": "URLs bzw. @-Handles zu deinen Social Media-Accounts. Werden verwendet, um Links zu deinen Accounts in den Metadaten zu setzen.", + "page.meta.headline": "SEO-Einstellungen", + "page.og.headline": "Open Graph-Einstellungen", + "fields.titleOverwrite.label": "Titel (Überschreiben)", + "fields.inheritSettings.label": "Einstellungen vererben", + "fields.inheritSettings.help": "Wähle aus, welche Einstellungen an Unterseiten vererbt werden sollen.\nDies kann z.B. hilfreich sein, wenn alle Beiträge eines Blogs ein eigenes Titel-Template haben sollen, welches vom Seiten-Standard abweicht. Alle Einstellungen lassen sich weiterhin in der Hauptseite überschreiben.", + "fields.useTitleTemplate.label": "Titel-Template verwenden?", + "fields.useTitleTemplate.no": "Nein - reiner Titel", + "fields.useTitleTemplate.yes": "Ja - mit Template", + "fields.useTitleTemplate.help": "Gibt an, ob das Titel-Template verwendet werden soll. Wird nicht vererbt.", + "writerNodes.template.title": "Seitentitel", + "writerNodes.template.siteTitle": "Website-Titel", + "common.default": "Standard:", + "common.yes": "Ja", + "common.no": "Nein", + "sitemap.title": "Sitemap", + "sitemap.index": "Sitemap-Index", + "sitemap.description": "Dies ist die Sitemap für deine Website, die Suchmaschinen über die Seiten auf deiner Website informiert, die indexiert werden können.", + "sitemap.by": "von", + "sitemap.changefreq": "Änderungsfrequenz", + "sitemap.lastUpdated": "Letzte Änderung", + "sitemap.priority": "Priorität", + "sitemap.url": "URL", + "sitemap.noEntries": "Keine Einträge", + "utmShare.button": "Teilen", + "utmShare.parameters": "Parameter", + "utmShare.source.label": "Quelle", + "utmShare.source.placeholder": "z. B. google, newsletter", + "utmShare.medium.label": "Medium", + "utmShare.medium.placeholder": "z. B. cpc, email, social", + "utmShare.campaign.label": "Kampagne", + "utmShare.campaign.placeholder": "z. B. fruehjahrsverkauf", + "utmShare.content.label": "Inhalt", + "utmShare.content.placeholder": "z. B. logo_link", + "utmShare.term.label": "Begriff", + "utmShare.term.placeholder": "z. B. laufschuhe", + "utmShare.ref.label": "Ref", + "utmShare.ref.placeholder": "z. B. getkirby.com", + "ai.action.generate": "Mit KI generieren", + "ai.action.regenerate": "Neu generieren", + "ai.action.edit": "Bearbeiten...", + "ai.action.customize": "KI-Generierung anpassen...", + "ai.action.stop": "Stoppen", + "ai.error.request": "Die KI-Anfrage ist fehlgeschlagen. Bitte versuche es erneut.", + "ai.error.disabled": "KI-Funktionen sind deaktiviert.", + "ai.error.permission": "Du hast keine Berechtigung, KI-Funktionen zu nutzen.", + "ai.dialog.instructions.label": "Anweisungen", + "ai.dialog.instructions.placeholder": "Welche Änderungen möchtest du am Text vornehmen?", + "ai.dialog.edit.submit": "Änderungen übernehmen", + "ai.dialog.custom.label": "Benutzerdefinierte Anweisungen", + "ai.dialog.custom.placeholder": "Welche Art von Inhalt möchtest du generieren?", + "ai.dialog.custom.submit": "Generieren", + "sections.searchConsole.title": "Google Search Console", + "sections.searchConsole.noCredentials": "Richte Google Cloud-Zugangsdaten ein, um die Search Console zu verbinden. Siehe die Dokumentation für Einrichtungsanweisungen.", + "sections.searchConsole.notConnected": "Verbinde dein Google-Konto, um Suchdaten für diese Seite zu sehen.", + "sections.searchConsole.selectProperty": "Wähle aus, welche Search Console-Property verwendet werden soll.", + "sections.searchConsole.selectPropertyButton": "Property auswählen", + "sections.searchConsole.selectPropertyLabel": "Property", + "sections.searchConsole.scDomain": "Domain", + "sections.searchConsole.docs": "Dokumentation", + "sections.searchConsole.connect": "Verbinden", + "sections.searchConsole.reconnect": "Erneut verbinden", + "sections.searchConsole.noData": "Keine Suchdaten für diese Seite verfügbar.", + "sections.searchConsole.showMore": "Alle anzeigen", + "sections.searchConsole.sortBy": "Sortieren nach", + "sections.searchConsole.query": "Suchanfrage", + "sections.searchConsole.clicks": "Klicks", + "sections.searchConsole.impressions": "Impressionen", + "sections.searchConsole.ctr": "CTR", + "sections.searchConsole.position": "Position", + "sections.searchConsole.openInGsc": "In Search Console öffnen" +} diff --git a/site/plugins/kirby-seo/translations/en.json b/site/plugins/kirby-seo/translations/en.json new file mode 100644 index 0000000..086c7d6 --- /dev/null +++ b/site/plugins/kirby-seo/translations/en.json @@ -0,0 +1,117 @@ +{ + "sections.headingStructure.title": "Heading Structure", + "sections.headingStructure.errors.incorrectOrder": "Your heading structure has an incorrect order and is invalid.", + "sections.headingStructure.errors.missingH1": "Your heading structure does not contain an H1 and is invalid.", + "sections.headingStructure.errors.multipleH1": "Your heading structure contains more than one H1 and is invalid.", + "sections.preview.title": "Preview", + "sections.preview.titleWithPage": "Preview (shows \"{title}\")", + "sections.preview.viewPage": "View page", + "sections.preview.showFor": "Show me", + "sections.preview.openDebugger": "Open Sharing Debugger", + "sections.preview.google": "Google", + "sections.preview.facebook": "Facebook", + "sections.preview.slack": "Slack", + "fields.robots.indicator.index": "Indexing allowed", + "fields.robots.indicator.any": "Indexing partly forbidden", + "fields.robots.indicator.noindex": "Indexing forbidden", + "fields.robots.label": "Robots Directives", + "fields.robots.index.label": "Indexing", + "fields.robots.index.help": "Whether search engines may index this page.", + "fields.robots.follow.label": "Follow Links", + "fields.robots.follow.help": "Whether search engines may follow links on this page.", + "fields.robots.archive.label": "Archive", + "fields.robots.archive.help": "Whether search engines may archive this page.", + "fields.robots.imageindex.label": "Image Indexing", + "fields.robots.imageindex.help": "Whether search engines may index images on this page.", + "fields.robots.snippet.label": "Snippets", + "fields.robots.snippet.help": "Whether search engines may show text snippets of this page.", + "tabs.seo": "Metadata & SEO", + "site.meta.headline": "Global SEO Settings", + "site.meta.headline.help": "These settings are used for all pages that do not have their own metadata.\nYou can override them for each page.", + "fields.metaTitleTemplate.label": "Title Template", + "fields.metaTitleTemplate.help": "A template to use for all page titles.", + "fields.metaDescription.label": "Page Description", + "fields.metaDescription.help": "Recommended length of 150 characters max. Used if no page description is specified.", + "site.og.headline": "Global Open Graph Settings", + "site.og.headline.help": "Set how your website appears when shared on social networks like Facebook or Twitter.", + "fields.ogTitleTemplate.label": "Open Graph Title Template", + "fields.ogDescription.label": "Open Graph Description", + "fields.ogSiteName.label": "Open Graph Site Name", + "fields.ogImage.label": "Open Graph Image", + "fields.ogImage.help": "Recommended size of 1200x630 pixels.", + "fields.ogImage.empty": "No Open Graph Image selected", + "fields.cropOgImage.label": "Crop OG Image to recommended size?", + "fields.cropOgImage.help": "Recommended size is 1200x630px. When enabled, images will be automatically cropped to this size for optimal display on social media.", + "fields.socialMediaAccounts.label": "Social Media Accounts", + "fields.socialMediaAccounts.help": "URLs or @handles to your social media accounts. Used to put links to your accounts in the metadata.", + "page.meta.headline": "SEO Settings", + "page.og.headline": "Open Graph Settings", + "fields.titleOverwrite.label": "Title (overwrite)", + "fields.inheritSettings.label": "Inherit Settings", + "fields.inheritSettings.help": "Select which settings should be inherited by subpages.\nThis can be helpful, for example, if all posts of a blog should have their own title template, which differs from the page default. All settings can still be overridden in the main page.", + "fields.useTitleTemplate.label": "Use title template?", + "fields.useTitleTemplate.no": "No - only title", + "fields.useTitleTemplate.yes": "Yes - with template", + "fields.useTitleTemplate.help": "Specifies whether the title template should be used. Will not be inherited.", + "writerNodes.template.title": "Page Title", + "writerNodes.template.siteTitle": "Site Title", + "common.default": "Default:", + "common.yes": "Yes", + "common.no": "No", + "sitemap.title": "Sitemap", + "sitemap.index": "Sitemap Index", + "sitemap.description": "This is the sitemap for your website that informs search engines about the pages on your website that can be indexed.", + "sitemap.by": "by", + "sitemap.changefreq": "Change Frequency", + "sitemap.lastUpdated": "Last Updated", + "sitemap.priority": "Priority", + "sitemap.url": "URL", + "sitemap.noEntries": "No entries", + "utmShare.button": "Share", + "utmShare.parameters": "Parameters", + "utmShare.source.label": "Source", + "utmShare.source.placeholder": "e.g. google, newsletter", + "utmShare.medium.label": "Medium", + "utmShare.medium.placeholder": "e.g. cpc, email, social", + "utmShare.campaign.label": "Campaign", + "utmShare.campaign.placeholder": "e.g. spring_sale", + "utmShare.content.label": "Content", + "utmShare.content.placeholder": "e.g. logo_link", + "utmShare.term.label": "Term", + "utmShare.term.placeholder": "e.g. running shoes", + "utmShare.ref.label": "Ref", + "utmShare.ref.placeholder": "e.g. getkirby.com", + "ai.action.generate": "Generate with AI", + "ai.action.regenerate": "Regenerate", + "ai.action.edit": "Edit…", + "ai.action.customize": "Customize AI generation…", + "ai.action.stop": "Stop", + "ai.error.request": "The AI request failed. Please try again.", + "ai.error.disabled": "AI features are disabled.", + "ai.error.permission": "You do not have the permission to use AI features.", + "ai.dialog.instructions.label": "Instructions", + "ai.dialog.instructions.placeholder": "What changes do you want to make to the text?", + "ai.dialog.edit.submit": "Apply Changes", + "ai.dialog.custom.label": "Custom Instructions", + "ai.dialog.custom.placeholder": "What kind of content do you want to generate?", + "ai.dialog.custom.submit": "Generate", + "sections.searchConsole.title": "Google Search Console", + "sections.searchConsole.noCredentials": "Set up Google Cloud credentials to connect Search Console. See the docs for setup instructions.", + "sections.searchConsole.notConnected": "Connect your Google account to see search data for this page.", + "sections.searchConsole.selectProperty": "Select which Search Console property to use.", + "sections.searchConsole.selectPropertyButton": "Select Property", + "sections.searchConsole.selectPropertyLabel": "Property", + "sections.searchConsole.scDomain": "domain", + "sections.searchConsole.docs": "Docs", + "sections.searchConsole.connect": "Connect", + "sections.searchConsole.reconnect": "Reconnect", + "sections.searchConsole.noData": "No search data available for this page.", + "sections.searchConsole.showMore": "Show all", + "sections.searchConsole.sortBy": "Sort by", + "sections.searchConsole.query": "Query", + "sections.searchConsole.clicks": "Clicks", + "sections.searchConsole.impressions": "Impressions", + "sections.searchConsole.ctr": "CTR", + "sections.searchConsole.position": "Position", + "sections.searchConsole.openInGsc": "Open in Search Console" +} diff --git a/site/plugins/kirby-seo/translations/fr.json b/site/plugins/kirby-seo/translations/fr.json new file mode 100644 index 0000000..70d6cf4 --- /dev/null +++ b/site/plugins/kirby-seo/translations/fr.json @@ -0,0 +1,117 @@ +{ + "sections.headingStructure.title": "Structure des titres", + "sections.headingStructure.errors.incorrectOrder": "Votre structure de titres a un ordre incorrect et est invalide.", + "sections.headingStructure.errors.missingH1": "Votre structure de titres ne contient pas de balise H1 et est invalide.", + "sections.headingStructure.errors.multipleH1": "Votre structure de titres contient plus d'une balise H1 et est invalide.", + "sections.preview.title": "Aperçu", + "sections.preview.titleWithPage": "Aperçu (affiche « {title} »)", + "sections.preview.viewPage": "Voir la page", + "sections.preview.showFor": "Montre-moi", + "sections.preview.openDebugger": "Ouvrir le débogueur de partage", + "sections.preview.google": "Google", + "sections.preview.facebook": "Facebook", + "sections.preview.slack": "Slack", + "fields.robots.indicator.index": "Indexation autorisée", + "fields.robots.indicator.any": "Indexation partiellement interdite", + "fields.robots.indicator.noindex": "Indexation interdite", + "fields.robots.label": "Directives des robots", + "fields.robots.index.label": "Indexation", + "fields.robots.index.help": "Indique si les moteurs de recherche peuvent indexer cette page.", + "fields.robots.follow.label": "Suivre les liens", + "fields.robots.follow.help": "Indique si les moteurs de recherche peuvent suivre les liens de cette page.", + "fields.robots.archive.label": "Archiver", + "fields.robots.archive.help": "Indique si les moteurs de recherche peuvent archiver cette page.", + "fields.robots.imageindex.label": "Indexation des images", + "fields.robots.imageindex.help": "Indique si les moteurs de recherche peuvent indexer les images de cette page.", + "fields.robots.snippet.label": "Extraits", + "fields.robots.snippet.help": "Indique si les moteurs de recherche peuvent afficher des extraits de texte de cette page.", + "tabs.seo": "Métadonnées & SEO", + "site.meta.headline": "Paramètres SEO globaux", + "site.meta.headline.help": "Ces paramètres sont utilisés pour toutes les pages qui n'ont pas leurs propres métadonnées.\nVous pouvez les remplacer pour chaque page.", + "fields.metaTitleTemplate.label": "Modèle de titre", + "fields.metaTitleTemplate.help": "Un modèle à utiliser pour tous les titres de page.", + "fields.metaDescription.label": "Description de la page", + "fields.metaDescription.help": "Longueur recommandée de 150 caractères maximum. Utilisée si aucune description de page n'est spécifiée.", + "site.og.headline": "Paramètres globaux Open Graph", + "site.og.headline.help": "Définissez l'apparence de votre site web lorsqu'il est partagé sur les réseaux sociaux tels que Facebook ou Twitter.", + "fields.ogTitleTemplate.label": "Modèle de titre Open Graph", + "fields.ogDescription.label": "Description Open Graph", + "fields.ogSiteName.label": "Nom du site Open Graph", + "fields.ogImage.label": "Image Open Graph", + "fields.ogImage.help": "Taille recommandée de 1200x630 pixels.", + "fields.ogImage.empty": "Aucune image Open Graph sélectionnée", + "fields.cropOgImage.label": "Recadrer l'image OG à la taille recommandée?", + "fields.cropOgImage.help": "La taille recommandée est de 1200x630px. Lorsqu'activé, les images seront automatiquement recadrées à cette taille pour un affichage optimal sur les réseaux sociaux.", + "fields.socialMediaAccounts.label": "Comptes de réseaux sociaux", + "fields.socialMediaAccounts.help": "URLs ou @nom_utilisateur de vos comptes de réseaux sociaux. Utilisés pour mettre des liens vers vos comptes dans les métadonnées.​", + "page.meta.headline": "Paramètres SEO", + "page.og.headline": "Paramètres Open Graph", + "fields.titleOverwrite.label": "Titre (remplacement)", + "fields.inheritSettings.label": "Hériter des paramètres", + "fields.inheritSettings.help": "Sélectionnez les paramètres à hériter par les sous-pages.\nCela peut être utile, par exemple, si tous les articles d'un blog doivent avoir leur propre modèle de titre, différent de celui de la page par défaut. Tous les paramètres peuvent toujours être remplacés dans la page principale.", + "fields.useTitleTemplate.label": "Utiliser le modèle de titre ?", + "fields.useTitleTemplate.no": "Non - seulement le titre", + "fields.useTitleTemplate.yes": "Oui - avec le modèle", + "fields.useTitleTemplate.help": "Indique si le modèle de titre doit être utilisé. Ne sera pas hérité.", + "writerNodes.template.title": "Titre de la page", + "writerNodes.template.siteTitle": "Titre du site", + "common.default": "Par défaut :", + "common.yes": "Oui", + "common.no": "Non", + "sitemap.title": "Sitemap", + "sitemap.index": "Index de sitemap", + "sitemap.description": "Il s'agit du sitemap de votre site web qui informe les moteurs de recherche des pages de votre site web qui peuvent être indexées.", + "sitemap.by": "par", + "sitemap.changefreq": "Fréquence de changement", + "sitemap.lastUpdated": "Dernière mise à jour", + "sitemap.priority": "Priorité", + "sitemap.url": "URL", + "sitemap.noEntries": "Aucune entrée", + "utmShare.button": "Partager", + "utmShare.parameters": "Paramètres", + "utmShare.source.label": "Source", + "utmShare.source.placeholder": "ex. google, newsletter", + "utmShare.medium.label": "Support", + "utmShare.medium.placeholder": "ex. cpc, email, social", + "utmShare.campaign.label": "Campagne", + "utmShare.campaign.placeholder": "ex. soldes_printemps", + "utmShare.content.label": "Contenu", + "utmShare.content.placeholder": "ex. lien_logo", + "utmShare.term.label": "Terme", + "utmShare.term.placeholder": "ex. chaussures de course", + "utmShare.ref.label": "Réf", + "utmShare.ref.placeholder": "ex. getkirby.com", + "ai.action.generate": "Générer avec l'IA", + "ai.action.regenerate": "Régénérer", + "ai.action.edit": "Modifier...", + "ai.action.customize": "Personnaliser la génération IA...", + "ai.action.stop": "Arrêter", + "ai.error.request": "La requête IA a échoué. Veuillez réessayer.", + "ai.error.disabled": "Les fonctionnalités d'IA sont désactivées.", + "ai.error.permission": "Vous n'avez pas la permission d'utiliser les fonctionnalités d'IA.", + "ai.dialog.instructions.label": "Instructions", + "ai.dialog.instructions.placeholder": "Quelles modifications souhaitez-vous apporter au texte ?", + "ai.dialog.edit.submit": "Appliquer les modifications", + "ai.dialog.custom.label": "Instructions personnalisées", + "ai.dialog.custom.placeholder": "Quel type de contenu souhaitez-vous générer ?", + "ai.dialog.custom.submit": "Générer", + "sections.searchConsole.title": "Google Search Console", + "sections.searchConsole.noCredentials": "Configure les identifiants Google Cloud pour connecter Search Console. Consulte la documentation pour les instructions de configuration.", + "sections.searchConsole.notConnected": "Connecte ton compte Google pour voir les données de recherche de cette page.", + "sections.searchConsole.selectProperty": "Sélectionne la propriété Search Console à utiliser.", + "sections.searchConsole.selectPropertyButton": "Sélectionner la propriété", + "sections.searchConsole.selectPropertyLabel": "Propriété", + "sections.searchConsole.scDomain": "domaine", + "sections.searchConsole.docs": "Documentation", + "sections.searchConsole.connect": "Connecter", + "sections.searchConsole.reconnect": "Reconnecter", + "sections.searchConsole.noData": "Aucune donnée de recherche disponible pour cette page.", + "sections.searchConsole.showMore": "Tout afficher", + "sections.searchConsole.sortBy": "Trier par", + "sections.searchConsole.query": "Requête", + "sections.searchConsole.clicks": "Clics", + "sections.searchConsole.impressions": "Impressions", + "sections.searchConsole.ctr": "CTR", + "sections.searchConsole.position": "Position", + "sections.searchConsole.openInGsc": "Ouvrir dans Search Console" +} diff --git a/site/plugins/kirby-seo/translations/nl.json b/site/plugins/kirby-seo/translations/nl.json new file mode 100644 index 0000000..6edaffc --- /dev/null +++ b/site/plugins/kirby-seo/translations/nl.json @@ -0,0 +1,117 @@ +{ + "sections.headingStructure.title": "Heading structuur", + "sections.headingStructure.errors.incorrectOrder": "Je heading structuur heeft een onjuiste volgorde en is ongeldig.", + "sections.headingStructure.errors.missingH1": "Je heading structuur bevat geen H1 en is ongeldig.", + "sections.headingStructure.errors.multipleH1": "Je heading structuur bevat meer dan één H1 en is ongeldig.", + "sections.preview.title": "Voorvertoning", + "sections.preview.titleWithPage": "Voorbeeld (toont \"{title}\")", + "sections.preview.viewPage": "Pagina bekijken", + "sections.preview.showFor": "Voorbeeld op", + "sections.preview.openDebugger": "Open Sharing Debugger", + "sections.preview.google": "Google", + "sections.preview.facebook": "Facebook", + "sections.preview.slack": "Slack", + "fields.robots.indicator.index": "Indexeren toegestaan", + "fields.robots.indicator.any": "Indexeren gedeeltelijk niet toegestaan", + "fields.robots.indicator.noindex": "Indexeren niet toegestaan", + "fields.robots.label": "Robots Directives", + "fields.robots.index.label": "Indexeren", + "fields.robots.index.help": "Of zoekmachines deze pagina mogen indexeren.", + "fields.robots.follow.label": "Volg Links", + "fields.robots.follow.help": "Of zoekmachines links op deze pagina mogen volgen.", + "fields.robots.archive.label": "Archiveren", + "fields.robots.archive.help": "Of zoekmachines deze pagina mogen archiveren.", + "fields.robots.imageindex.label": "Afbeeldingen indexeren", + "fields.robots.imageindex.help": "Of zoekmachines afbeeldingen op deze pagina mogen indexeren.", + "fields.robots.snippet.label": "Fragmenten", + "fields.robots.snippet.help": "Of zoekmachines tekstfragmenten van deze pagina mogen tonen.", + "tabs.seo": "Metadata & SEO", + "site.meta.headline": "Globale SEO Instellingen", + "site.meta.headline.help": "Deze instellingen worden gebruikt voor alle pagina's die geen eigen metadata hebben.\nJe kunt ze voor elke pagina overschrijven.", + "fields.metaTitleTemplate.label": "Titel template", + "fields.metaTitleTemplate.help": "Een sjabloon voor alle paginatitels.", + "fields.metaDescription.label": "Paginabeschrijving", + "fields.metaDescription.help": "Aanbevolen lengte van maximaal 150 tekens. Gebruikt als er geen paginabeschrijving is opgegeven.", + "site.og.headline": "Globale Open Graph Instellingen", + "site.og.headline.help": "Bepaal hoe je website verschijnt wanneer deze wordt gedeeld op sociale netwerken zoals Facebook of Twitter.", + "fields.ogTitleTemplate.label": "Open Graph Titel Template", + "fields.ogDescription.label": "Open Graph Beschrijving", + "fields.ogSiteName.label": "Open Graph Sitenaam", + "fields.ogImage.label": "Open Graph Afbeelding", + "fields.ogImage.help": "Aanbevolen grootte van 1200x630 pixels.", + "fields.ogImage.empty": "Geen Open Graph Afbeelding geselecteerd", + "fields.cropOgImage.label": "OG-afbeelding bijsnijden naar aanbevolen grootte?", + "fields.cropOgImage.help": "Aanbevolen grootte is 1200x630px. Indien ingeschakeld, worden afbeeldingen automatisch bijgesneden naar deze grootte voor optimale weergave op sociale media.", + "fields.socialMediaAccounts.label": "Sociale Media Accounts", + "fields.socialMediaAccounts.help": "URL's of @handles naar je sociale media accounts. Gebruikt om links naar je accounts in de metadata te plaatsen.", + "page.meta.headline": "SEO-instellingen", + "page.og.headline": "Open Graph-instellingen", + "fields.titleOverwrite.label": "Titel (overschrijven)", + "fields.inheritSettings.label": "Instellingen Overerven", + "fields.inheritSettings.help": "Selecteer welke instellingen door subpagina's moeten worden geërfd.\nDit kan nuttig zijn, bijvoorbeeld als alle berichten van een blog hun eigen titelsjabloon moeten hebben, die verschilt van de standaard van de pagina. Alle instellingen kunnen nog steeds worden overschreven op de hoofdpagina.", + "fields.useTitleTemplate.label": "Titelsjabloon gebruiken?", + "fields.useTitleTemplate.no": "Nee - alleen titel", + "fields.useTitleTemplate.yes": "Ja - met sjabloon", + "fields.useTitleTemplate.help": "Geeft aan of het titelsjabloon moet worden gebruikt. Wordt niet geërfd.", + "writerNodes.template.title": "Paginatitel", + "writerNodes.template.siteTitle": "Sitenaam", + "common.default": "Standaard:", + "common.yes": "Ja", + "common.no": "Nee", + "sitemap.title": "Sitemap", + "sitemap.index": "Sitemap Index", + "sitemap.description": "Dit is de sitemap voor je website die zoekmachines informeert over de pagina's op je website die geïndexeerd kunnen worden.", + "sitemap.by": "door", + "sitemap.changefreq": "Veranderingsfrequentie", + "sitemap.lastUpdated": "Laatst bijgewerkt", + "sitemap.priority": "Prioriteit", + "sitemap.url": "URL", + "sitemap.noEntries": "Geen vermeldingen", + "utmShare.button": "Delen", + "utmShare.parameters": "Parameters", + "utmShare.source.label": "Bron", + "utmShare.source.placeholder": "bijv. google, nieuwsbrief", + "utmShare.medium.label": "Medium", + "utmShare.medium.placeholder": "bijv. cpc, email, social", + "utmShare.campaign.label": "Campagne", + "utmShare.campaign.placeholder": "bijv. lente_uitverkoop", + "utmShare.content.label": "Content", + "utmShare.content.placeholder": "bijv. logo_link", + "utmShare.term.label": "Zoekterm", + "utmShare.term.placeholder": "bijv. hardloopschoenen", + "utmShare.ref.label": "Ref", + "utmShare.ref.placeholder": "bijv. getkirby.com", + "ai.action.generate": "Genereer met AI", + "ai.action.regenerate": "Opnieuw genereren", + "ai.action.edit": "Bewerken...", + "ai.action.customize": "AI-generatie aanpassen...", + "ai.action.stop": "Stoppen", + "ai.error.request": "Het AI-verzoek is mislukt. Probeer het opnieuw.", + "ai.error.disabled": "AI-functies zijn uitgeschakeld.", + "ai.error.permission": "Je hebt geen toestemming om AI-functies te gebruiken.", + "ai.dialog.instructions.label": "Instructies", + "ai.dialog.instructions.placeholder": "Welke wijzigingen wil je aanbrengen in de tekst?", + "ai.dialog.edit.submit": "Wijzigingen toepassen", + "ai.dialog.custom.label": "Aangepaste instructies", + "ai.dialog.custom.placeholder": "Wat voor soort content wil je genereren?", + "ai.dialog.custom.submit": "Genereren", + "sections.searchConsole.title": "Google Search Console", + "sections.searchConsole.noCredentials": "Stel Google Cloud-inloggegevens in om Search Console te verbinden. Bekijk de documentatie voor installatie-instructies.", + "sections.searchConsole.notConnected": "Verbind je Google-account om zoekgegevens voor deze pagina te bekijken.", + "sections.searchConsole.selectProperty": "Selecteer welke Search Console-property je wilt gebruiken.", + "sections.searchConsole.selectPropertyButton": "Selecteer property", + "sections.searchConsole.selectPropertyLabel": "Property", + "sections.searchConsole.scDomain": "domein", + "sections.searchConsole.docs": "Documentatie", + "sections.searchConsole.connect": "Verbinden", + "sections.searchConsole.reconnect": "Opnieuw verbinden", + "sections.searchConsole.noData": "Geen zoekgegevens beschikbaar voor deze pagina.", + "sections.searchConsole.showMore": "Alles tonen", + "sections.searchConsole.sortBy": "Sorteer op", + "sections.searchConsole.query": "Zoekopdracht", + "sections.searchConsole.clicks": "Klikken", + "sections.searchConsole.impressions": "Vertoningen", + "sections.searchConsole.ctr": "CTR", + "sections.searchConsole.position": "Positie", + "sections.searchConsole.openInGsc": "Openen in Search Console" +} diff --git a/site/plugins/kirby-seo/translations/pt_PT.json b/site/plugins/kirby-seo/translations/pt_PT.json new file mode 100644 index 0000000..37e347c --- /dev/null +++ b/site/plugins/kirby-seo/translations/pt_PT.json @@ -0,0 +1,117 @@ +{ + "sections.headingStructure.title": "Estrutura de Títulos", + "sections.headingStructure.errors.incorrectOrder": "A estrutura de títulos tem uma ordem incorrecta e é inválida.", + "sections.headingStructure.errors.missingH1": "A estrutura de títulos não contém uma tag H1 e é inválida.", + "sections.headingStructure.errors.multipleH1": "A estrutura de títulos contém mais do que uma tag H1 e é inválida.", + "sections.preview.title": "Pré-visualização", + "sections.preview.titleWithPage": "Pré-visualização (mostra \"{title}\")", + "sections.preview.viewPage": "Ver página", + "sections.preview.showFor": "Mostrar", + "sections.preview.openDebugger": "Abrir Sharing Debugger", + "sections.preview.google": "Google", + "sections.preview.facebook": "Facebook", + "sections.preview.slack": "Slack", + "fields.robots.indicator.index": "Indexação permitida", + "fields.robots.indicator.any": "Indexação parcialmente proibida", + "fields.robots.indicator.noindex": "Indexação proibida", + "fields.robots.label": "Diretivas Robots", + "fields.robots.index.label": "Indexação", + "fields.robots.index.help": "Se os motores de pesquisa podem indexar esta página.", + "fields.robots.follow.label": "Seguir Links", + "fields.robots.follow.help": "Se os motores de pesquisa podem seguir links nesta página.", + "fields.robots.archive.label": "Arquivo", + "fields.robots.archive.help": "Se os motores de pesquisa podem arquivar esta página.", + "fields.robots.imageindex.label": "Indexação de Imagens", + "fields.robots.imageindex.help": "Se os motores de pesquisa podem indexar imagens desta página.", + "fields.robots.snippet.label": "Snippets", + "fields.robots.snippet.help": "Se os motores de pesquisa podem mostrar snippets de texto desta página.", + "tabs.seo": "Metadados & SEO", + "site.meta.headline": "Configurações Globais SEO", + "site.meta.headline.help": "Estas configurações são usadas para todas as páginas que não têm os seus próprios metadados.\nPode substituí-las em cada página.", + "fields.metaTitleTemplate.label": "Template de Título", + "fields.metaTitleTemplate.help": "Um modelo para usar em todos os títulos de página.", + "fields.metaDescription.label": "Descrição de Página", + "fields.metaDescription.help": "Recomendado um tamanho de 150 caracteres no máximo. Usada se nenhuma descrição de página for especificada.", + "site.og.headline": "Configurações Globais Open Graph", + "site.og.headline.help": "Defina como o seu site aparece quando é partilhado em redes sociais como o Facebook ou o Twitter.", + "fields.ogTitleTemplate.label": "Template de Título Open Graph", + "fields.ogDescription.label": "Descrição Open Graph", + "fields.ogSiteName.label": "Nome do Site Open Graph", + "fields.ogImage.label": "Imagem Open Graph", + "fields.ogImage.help": "Tamanho recomendado de 1200x630 pixels.", + "fields.ogImage.empty": "Nenhuma Imagem Open Graph selecionada", + "fields.cropOgImage.label": "Cortar imagem OG para tamanho recomendado?", + "fields.cropOgImage.help": "O tamanho recomendado é 1200x630px. Quando ativado, as imagens serão automaticamente cortadas para este tamanho para exibição ideal em redes sociais.", + "fields.socialMediaAccounts.label": "Contas de Redes Sociais", + "fields.socialMediaAccounts.help": "URLs ou @handles para as suas contas de redes sociais. Usado para colocar links para as suas contas nos metadados.", + "page.meta.headline": "Definições de SEO", + "page.og.headline": "Definições de Open Graph", + "fields.titleOverwrite.label": "Título (substituir)", + "fields.inheritSettings.label": "Herdar Configurações", + "fields.inheritSettings.help": "Selecione quais as configurações que devem ser herdadas pelas subpáginas.\nIsto pode ser útil, por exemplo, se todos os posts de um blog tiverem o seu próprio template de título, que pode ser diferente do pré-configurado na página. Todas as configurações continuam a poder ser substituídas na página principal.", + "fields.useTitleTemplate.label": "Usar o template de título?", + "fields.useTitleTemplate.no": "Não - apenas o título", + "fields.useTitleTemplate.yes": "Sim - com template", + "fields.useTitleTemplate.help": "Especifica se o template de título deve ser usado. Não será herdado.", + "writerNodes.template.title": "Título da página", + "writerNodes.template.siteTitle": "Título do site", + "common.default": "Por defeito:", + "common.yes": "Sim", + "common.no": "Não", + "sitemap.title": "Sitemap", + "sitemap.index": "Índice Sitemap", + "sitemap.description": "Este é o sitemap do site que informa os motores de pesquisa sobre as páginas que podem ser indexadas.", + "sitemap.by": "por", + "sitemap.changefreq": "Frequência de Mudança", + "sitemap.lastUpdated": "Última Atualização", + "sitemap.priority": "Prioridade", + "sitemap.url": "URL", + "sitemap.noEntries": "Sem registos", + "utmShare.button": "Partilhar", + "utmShare.parameters": "Parâmetros", + "utmShare.source.label": "Origem", + "utmShare.source.placeholder": "ex: google, newsletter", + "utmShare.medium.label": "Meio", + "utmShare.medium.placeholder": "ex: cpc, email, social", + "utmShare.campaign.label": "Campanha", + "utmShare.campaign.placeholder": "ex: saldos_primavera", + "utmShare.content.label": "Conteúdo", + "utmShare.content.placeholder": "ex: link_logotipo", + "utmShare.term.label": "Termo", + "utmShare.term.placeholder": "ex: ténis de corrida", + "utmShare.ref.label": "Ref", + "utmShare.ref.placeholder": "ex: getkirby.com", + "ai.action.generate": "Gerar com IA", + "ai.action.regenerate": "Regenerar", + "ai.action.edit": "Editar...", + "ai.action.customize": "Personalizar geração de IA...", + "ai.action.stop": "Parar", + "ai.error.request": "O pedido de IA falhou. Por favor, tenta novamente.", + "ai.error.disabled": "As funcionalidades de IA estão desativadas.", + "ai.error.permission": "Não tens permissão para usar as funcionalidades de IA.", + "ai.dialog.instructions.label": "Instruções", + "ai.dialog.instructions.placeholder": "Que alterações queres fazer ao texto?", + "ai.dialog.edit.submit": "Aplicar alterações", + "ai.dialog.custom.label": "Instruções personalizadas", + "ai.dialog.custom.placeholder": "Que tipo de conteúdo queres gerar?", + "ai.dialog.custom.submit": "Gerar", + "sections.searchConsole.title": "Google Search Console", + "sections.searchConsole.noCredentials": "Configura as credenciais do Google Cloud para conectar a Search Console. Consulta a documentação para instruções de configuração.", + "sections.searchConsole.notConnected": "Conecta a tua conta Google para veres os dados de pesquisa desta página.", + "sections.searchConsole.selectProperty": "Seleciona qual propriedade da Search Console queres usar.", + "sections.searchConsole.selectPropertyButton": "Selecionar propriedade", + "sections.searchConsole.selectPropertyLabel": "Propriedade", + "sections.searchConsole.scDomain": "domínio", + "sections.searchConsole.docs": "Documentação", + "sections.searchConsole.connect": "Conectar", + "sections.searchConsole.reconnect": "Reconectar", + "sections.searchConsole.noData": "Não há dados de pesquisa disponíveis para esta página.", + "sections.searchConsole.showMore": "Mostrar tudo", + "sections.searchConsole.sortBy": "Ordenar por", + "sections.searchConsole.query": "Consulta", + "sections.searchConsole.clicks": "Cliques", + "sections.searchConsole.impressions": "Impressões", + "sections.searchConsole.ctr": "CTR", + "sections.searchConsole.position": "Posição", + "sections.searchConsole.openInGsc": "Abrir na Search Console" +} diff --git a/site/plugins/kirby-seo/translations/ro.json b/site/plugins/kirby-seo/translations/ro.json new file mode 100644 index 0000000..5320502 --- /dev/null +++ b/site/plugins/kirby-seo/translations/ro.json @@ -0,0 +1,117 @@ +{ + "sections.headingStructure.title": "Structura de subtitluri", + "sections.headingStructure.errors.incorrectOrder": "Structura de subtitluri are ordinea incorectă, ceea ce o face invalidă.", + "sections.headingStructure.errors.missingH1": "Structura de subtitluri nu conține un H1, ceea ce o face invalidă.", + "sections.headingStructure.errors.multipleH1": "Structura de subtitluri conține mai mult de un H1, ceea ce o face invalidă.", + "sections.preview.title": "Previzualizare", + "sections.preview.titleWithPage": "Previzualizare (afișează „{title}\")", + "sections.preview.viewPage": "Vezi pagina", + "sections.preview.showFor": "Arată-mi", + "sections.preview.openDebugger": "Deschide Sharing Debugger", + "sections.preview.google": "Google", + "sections.preview.facebook": "Facebook", + "sections.preview.slack": "Slack", + "fields.robots.indicator.index": "Indexarea permisă", + "fields.robots.indicator.any": "Indexarea parțial interzisă", + "fields.robots.indicator.noindex": "Indexarea interzisă", + "fields.robots.label": "Directive Robots", + "fields.robots.index.label": "Indexare", + "fields.robots.index.help": "Dacă se permite motoarelor de căutare să indexeze această pagină.", + "fields.robots.follow.label": "Urmare linkuri", + "fields.robots.follow.help": "Dacă se permite motoarelor de căutare să urmeze linkurile de pe această pagină.", + "fields.robots.archive.label": "Arhivare", + "fields.robots.archive.help": "Dacă se permite motoarelor de căutare să arhiveze această pagină.", + "fields.robots.imageindex.label": "Indexare imagini", + "fields.robots.imageindex.help": "Dacă se permite motoarelor de căutare să indexeze imaginile de pe această pagină.", + "fields.robots.snippet.label": "Fragmente", + "fields.robots.snippet.help": "Dacă se permite motoarelor de căutare să afișeze fragmente din această pagină.", + "tabs.seo": "Metadata și SEO", + "site.meta.headline": "Setări globale SEO", + "site.meta.headline.help": "Aceste setări sunt folosite pentru toate paginile care nu au metadata proprie.\nEle se pot suprascrie pentru fiecare pagină în parte.", + "fields.metaTitleTemplate.label": "Șablon pentru titlu", + "fields.metaTitleTemplate.help": "Șablonul implicit folosit pentru toate paginile.", + "fields.metaDescription.label": "Descrierea paginii", + "fields.metaDescription.help": "Lungimea recomandată este de maxim 150 de caractere. Folosită dacă o pagină nu are propria descriere specificată.", + "site.og.headline": "Setări globale Open Graph", + "site.og.headline.help": "Stabilește cum apare website-ul atunci când este partajat pe rețelele sociale ca Facebook sau Twitter.", + "fields.ogTitleTemplate.label": "Șablon pentru titlu Open Graph", + "fields.ogDescription.label": "Descriere Open Graph", + "fields.ogSiteName.label": "Nume site Open Graph", + "fields.ogImage.label": "Imagine Open Graph", + "fields.ogImage.help": "Dimensiunea recomandată este de 1200x630 pixeli.", + "fields.ogImage.empty": "Nicio imagine Open Graph aleasă", + "fields.cropOgImage.label": "Decupează imaginea OG la dimensiunea recomandată?", + "fields.cropOgImage.help": "Dimensiunea recomandată este de 1200x630px. Când setarea este activă, imaginile vor fi decupate automat la această dimensiune pentru o afișare ideală pe rețelele sociale.", + "fields.socialMediaAccounts.label": "Conturi sociale", + "fields.socialMediaAccounts.help": "URL-uri sau @username-uri către conturile tale pe rețelele sociale. Folosite pentru a pune linkuri către aceste conturi în metadata.", + "page.meta.headline": "Setări SEO", + "page.og.headline": "Setări Open Graph", + "fields.titleOverwrite.label": "Titlu (suprascrie)", + "fields.inheritSettings.label": "Moștenește setări", + "fields.inheritSettings.help": "Alege ce setări să moștenească subpaginile.\nPoate fi util când, de exemplu, toate postările unui blog folosesc un șablon de titlu comun, dar care diferă de șablonul implicit. Setările pot fi suprascrise oricând în subpagină.", + "fields.useTitleTemplate.label": "Folosește șablon pentru titlu?", + "fields.useTitleTemplate.no": "Nu - doar titlul", + "fields.useTitleTemplate.yes": "Da - cu șablon", + "fields.useTitleTemplate.help": "Dacă să se folosească șablonul de titlu. Nu se moștenește.", + "writerNodes.template.title": "Titlul paginii", + "writerNodes.template.siteTitle": "Titlul website-ului", + "common.default": "Implicit:", + "common.yes": "Da", + "common.no": "Nu", + "sitemap.title": "Sitemap", + "sitemap.index": "Index sitemap-uri", + "sitemap.description": "Acesta este sitemap-ul care informează motoarele de căutare despre paginile din website care pot fi indexate.", + "sitemap.by": "de", + "sitemap.changefreq": "Schimbă frecvența", + "sitemap.lastUpdated": "Ultima actualizare", + "sitemap.priority": "Prioritate", + "sitemap.url": "URL", + "sitemap.noEntries": "Nicio înregistrare", + "utmShare.button": "Distribuie", + "utmShare.parameters": "Parametri", + "utmShare.source.label": "Sursă", + "utmShare.source.placeholder": "ex: google, newsletter", + "utmShare.medium.label": "Mediu", + "utmShare.medium.placeholder": "ex: cpc, email, social", + "utmShare.campaign.label": "Campanie", + "utmShare.campaign.placeholder": "ex: spring_sale", + "utmShare.content.label": "Conținut", + "utmShare.content.placeholder": "ex: logo_link", + "utmShare.term.label": "Termen", + "utmShare.term.placeholder": "ex: pantofi de alergare", + "utmShare.ref.label": "Ref", + "utmShare.ref.placeholder": "ex: getkirby.com", + "ai.action.generate": "Generează cu AI", + "ai.action.regenerate": "Regenerare", + "ai.action.edit": "Editează…", + "ai.action.customize": "Personalizează generarea AI…", + "ai.action.stop": "Oprește", + "ai.error.request": "Solicitarea AI a eșuat. Te rugăm să încerci din nou.", + "ai.error.disabled": "Funcțiile AI sunt dezactivate.", + "ai.error.permission": "Nu ai permisiunea de a utiliza funcțiile AI.", + "ai.dialog.instructions.label": "Instrucțiuni", + "ai.dialog.instructions.placeholder": "Ce modificări dorești să faci textului?", + "ai.dialog.edit.submit": "Aplică modificările", + "ai.dialog.custom.label": "Instrucțiuni personalizate", + "ai.dialog.custom.placeholder": "Ce tip de conținut dorești să generezi?", + "ai.dialog.custom.submit": "Generează", + "sections.searchConsole.title": "Google Search Console", + "sections.searchConsole.noCredentials": "Configurează acreditările Google Cloud pentru a conecta Search Console. Vezi documentația pentru instrucțiuni de configurare.", + "sections.searchConsole.notConnected": "Conectează-ți contul Google ca să vezi datele de căutare pentru această pagină.", + "sections.searchConsole.selectProperty": "Selectează ce proprietate Search Console vrei să folosești.", + "sections.searchConsole.selectPropertyButton": "Selectează proprietatea", + "sections.searchConsole.selectPropertyLabel": "Proprietate", + "sections.searchConsole.scDomain": "domeniu", + "sections.searchConsole.docs": "Documentație", + "sections.searchConsole.connect": "Conectează", + "sections.searchConsole.reconnect": "Reconectează", + "sections.searchConsole.noData": "Nu există date de căutare disponibile pentru această pagină.", + "sections.searchConsole.showMore": "Afișează tot", + "sections.searchConsole.sortBy": "Sortează după", + "sections.searchConsole.query": "Interogare", + "sections.searchConsole.clicks": "Clickuri", + "sections.searchConsole.impressions": "Afișări", + "sections.searchConsole.ctr": "CTR", + "sections.searchConsole.position": "Poziție", + "sections.searchConsole.openInGsc": "Deschide în Search Console" +} diff --git a/site/plugins/kirby-seo/translations/sv_SE.json b/site/plugins/kirby-seo/translations/sv_SE.json new file mode 100644 index 0000000..99c40c8 --- /dev/null +++ b/site/plugins/kirby-seo/translations/sv_SE.json @@ -0,0 +1,117 @@ +{ + "sections.headingStructure.title": "Rubrikstruktur", + "sections.headingStructure.errors.incorrectOrder": "Din rubrikstruktur har en felaktig ordning och är ogiltig.", + "sections.headingStructure.errors.missingH1": "Din rubrikstruktur innehåller ingen H1 och är ogiltig.", + "sections.headingStructure.errors.multipleH1": "Din rubrikstruktur innehåller mer än en H1 och är ogiltig.", + "sections.preview.title": "Förhandsvisning", + "sections.preview.titleWithPage": "Förhandsgranskning (visar \"{title}\")", + "sections.preview.viewPage": "Visa sida", + "sections.preview.showFor": "Visa mig", + "sections.preview.openDebugger": "Öppna Sharing Debugger", + "sections.preview.google": "Google", + "sections.preview.facebook": "Facebook", + "sections.preview.slack": "Slack", + "fields.robots.indicator.index": "Indexering tillåten", + "fields.robots.indicator.any": "Indexering delvis förbjuden", + "fields.robots.indicator.noindex": "Indexering förbjuden", + "fields.robots.label": "Robotdirektiv", + "fields.robots.index.label": "Indexering", + "fields.robots.index.help": "Om sökmotorer får indexera den här sidan.", + "fields.robots.follow.label": "Följ länkar", + "fields.robots.follow.help": "Om sökmotorer får följa länkar på den här sidan.", + "fields.robots.archive.label": "Arkiv", + "fields.robots.archive.help": "Om sökmotorer får arkivera den här sidan.", + "fields.robots.imageindex.label": "Bildindexering", + "fields.robots.imageindex.help": "Om sökmotorer får indexera bilder på den här sidan.", + "fields.robots.snippet.label": "Textutdrag", + "fields.robots.snippet.help": "Om sökmotorer får visa textutdrag från den här sidan.", + "tabs.seo": "Metadata och SEO", + "site.meta.headline": "Globala SEO-inställningar", + "site.meta.headline.help": "Dessa inställningar används för alla sidor som inte har egna metadata.\nDu kan åsidosätta dem för varje sida.", + "fields.metaTitleTemplate.label": "Titelmall", + "fields.metaTitleTemplate.help": "En mall att använda för alla sidtitlar.", + "fields.metaDescription.label": "Sidbeskrivning", + "fields.metaDescription.help": "Rekommenderad längd på max 150 tecken. Används om ingen sidbeskrivning anges.", + "site.og.headline": "Globala Open Graph-inställningar", + "site.og.headline.help": "Ställ in hur din webbplats visas när den delas på sociala nätverk som Facebook eller Twitter.", + "fields.ogTitleTemplate.label": "Open Graph titelmall", + "fields.ogDescription.label": "Open Graph beskrivning", + "fields.ogSiteName.label": "Open Graph webbplatsnamn", + "fields.ogImage.label": "Open Graph bild", + "fields.ogImage.help": "Rekommenderad storlek på 1200x630 pixlar.", + "fields.ogImage.empty": "Ingen Open Graph-bild har valts", + "fields.cropOgImage.label": "Beskär OG-bild till rekommenderad storlek?", + "fields.cropOgImage.help": "Rekommenderad storlek är 1200x630px. När aktiverad kommer bilder automatiskt beskäras till denna storlek för optimal visning på sociala medier.", + "fields.socialMediaAccounts.label": "Konton för sociala medier", + "fields.socialMediaAccounts.help": "URL:er eller @användarnamn till dina sociala mediekonton. Används för att lägga länkar till dina konton i metadata.", + "page.meta.headline": "SEO-inställningar", + "page.og.headline": "Open Graph-inställningar", + "fields.titleOverwrite.label": "Titel (skriv över)", + "fields.inheritSettings.label": "Ärv inställningar", + "fields.inheritSettings.help": "Välj vilka inställningar som ska ärvas av undersidor.\nDetta kan till exempel vara användbart om alla inlägg på en blogg ska ha en egen titelmall, som skiljer sig från sidans standard. Alla inställningar kan fortfarande åsidosättas på huvudsidan.", + "fields.useTitleTemplate.label": "Använd titelmall?", + "fields.useTitleTemplate.no": "Nej - bara titel", + "fields.useTitleTemplate.yes": "Ja - med mall", + "fields.useTitleTemplate.help": "Anger om titelmallen ska användas. Kommer inte att ärvas.", + "writerNodes.template.title": "Sidtitel", + "writerNodes.template.siteTitle": "Webbplatstitel", + "common.default": "Standard:", + "common.yes": "Ja", + "common.no": "Nej", + "sitemap.title": "Webbplatskarta", + "sitemap.index": "Webbplatskartans index", + "sitemap.description": "Detta är webbplatskartan för din webbplats som informerar sökmotorer om vilka sidor på din webbplats som kan indexeras.", + "sitemap.by": "av", + "sitemap.changefreq": "Ändra frekvens", + "sitemap.lastUpdated": "Senast uppdaterad", + "sitemap.priority": "Prioritet", + "sitemap.url": "URL", + "sitemap.noEntries": "Inga poster", + "utmShare.button": "Dela", + "utmShare.parameters": "Parametrar", + "utmShare.source.label": "Källa", + "utmShare.source.placeholder": "t.ex. google, nyhetsbrev", + "utmShare.medium.label": "Medium", + "utmShare.medium.placeholder": "t.ex. cpc, e-post, sociala medier", + "utmShare.campaign.label": "Kampanj", + "utmShare.campaign.placeholder": "t.ex. spring_sale", + "utmShare.content.label": "Innehåll", + "utmShare.content.placeholder": "t.ex. logo_link", + "utmShare.term.label": "Sökord", + "utmShare.term.placeholder": "t.ex. löparskor", + "utmShare.ref.label": "Ref", + "utmShare.ref.placeholder": "t.ex. getkirby.com", + "ai.action.generate": "Generera med AI", + "ai.action.regenerate": "Generera om", + "ai.action.edit": "Redigera…", + "ai.action.customize": "Anpassa AI-generering…", + "ai.action.stop": "Stoppa", + "ai.error.request": "AI-förfrågan misslyckades. Försök igen.", + "ai.error.disabled": "AI-funktioner är inaktiverade.", + "ai.error.permission": "Du har inte behörighet att använda AI-funktioner.", + "ai.dialog.instructions.label": "Instruktioner", + "ai.dialog.instructions.placeholder": "Vilka ändringar vill du göra i texten?", + "ai.dialog.edit.submit": "Tillämpa ändringar", + "ai.dialog.custom.label": "Anpassade instruktioner", + "ai.dialog.custom.placeholder": "Vilken typ av innehåll vill du generera?", + "ai.dialog.custom.submit": "Generera", + "sections.searchConsole.title": "Google Search Console", + "sections.searchConsole.noCredentials": "Ställ in Google Cloud-referenser för att ansluta Search Console. Se dokumentationen för installationsinstruktioner.", + "sections.searchConsole.notConnected": "Anslut ditt Google-konto för att se sökdata för den här sidan.", + "sections.searchConsole.selectProperty": "Välj vilken Search Console-egenskap du vill använda.", + "sections.searchConsole.selectPropertyButton": "Välj egenskap", + "sections.searchConsole.selectPropertyLabel": "Egenskap", + "sections.searchConsole.scDomain": "domän", + "sections.searchConsole.docs": "Dokumentation", + "sections.searchConsole.connect": "Anslut", + "sections.searchConsole.reconnect": "Anslut igen", + "sections.searchConsole.noData": "Ingen sökdata tillgänglig för den här sidan.", + "sections.searchConsole.showMore": "Visa alla", + "sections.searchConsole.sortBy": "Sortera efter", + "sections.searchConsole.query": "Sökfråga", + "sections.searchConsole.clicks": "Klick", + "sections.searchConsole.impressions": "Visningar", + "sections.searchConsole.ctr": "CTR", + "sections.searchConsole.position": "Position", + "sections.searchConsole.openInGsc": "Öppna i Search Console" +} diff --git a/site/plugins/kirby-seo/translations/tr.json b/site/plugins/kirby-seo/translations/tr.json new file mode 100644 index 0000000..ba071b9 --- /dev/null +++ b/site/plugins/kirby-seo/translations/tr.json @@ -0,0 +1,117 @@ +{ + "sections.headingStructure.title": "Başlık Yapısı", + "sections.headingStructure.errors.incorrectOrder": "Başlık yapınız yanlış bir sıraya sahip ve geçersizdir.", + "sections.headingStructure.errors.missingH1": "Başlık yapınızda bir H1 etiketi bulunmuyor ve geçersizdir.", + "sections.headingStructure.errors.multipleH1": "Başlık yapınızda birden fazla H1 etiketi bulunuyor ve geçersizdir.", + "sections.preview.title": "Önizleme", + "sections.preview.titleWithPage": "Önizleme (\"{title}\" gösteriliyor)", + "sections.preview.viewPage": "Sayfayı görüntüle", + "sections.preview.showFor": "Bana göster", + "sections.preview.openDebugger": "Paylaşım Hata Ayıklayıcıyı Aç", + "sections.preview.google": "Google", + "sections.preview.facebook": "Facebook", + "sections.preview.slack": "Slack", + "fields.robots.indicator.index": "İndekslemeye izin verildi", + "fields.robots.indicator.any": "İndeksleme kısmen yasaklandı", + "fields.robots.indicator.noindex": "İndeksleme yasaklandı", + "fields.robots.label": "Robot Talimatları", + "fields.robots.index.label": "İndeksleme", + "fields.robots.index.help": "Arama motorlarının bu sayfayı indeksleyip indekslemeyeceğini belirtir.", + "fields.robots.follow.label": "Bağlantıları Takip Et", + "fields.robots.follow.help": "Arama motorlarının bu sayfadaki bağlantıları takip edip etmeyeceğini belirtir.", + "fields.robots.archive.label": "Arşivle", + "fields.robots.archive.help": "Arama motorlarının bu sayfayı arşivleyip arşivlemeyeceğini belirtir.", + "fields.robots.imageindex.label": "İmaj İndeksleme", + "fields.robots.imageindex.help": "Arama motorlarının bu sayfadaki görüntüleri indeksleyip indekslemeyeceğini belirtir.", + "fields.robots.snippet.label": "Özetler", + "fields.robots.snippet.help": "Arama motorlarının bu sayfanın metin özetlerini gösterip göstermeyeceğini belirtir.", + "tabs.seo": "Meta Veriler & SEO", + "site.meta.headline": "Genel SEO Ayarları", + "site.meta.headline.help": "Bu ayarlar, kendi meta verileri olmayan tüm sayfalar için kullanılır.\nHer sayfa için bunları geçersiz kılabilirsiniz.", + "fields.metaTitleTemplate.label": "Başlık Şablonu", + "fields.metaTitleTemplate.help": "Tüm sayfa başlıkları için kullanılacak bir şablon.", + "fields.metaDescription.label": "Sayfa Açıklaması", + "fields.metaDescription.help": "Maksimum 150 karakter uzunluğunda önerilen bir sayfa açıklamasıdır. Sayfa açıklaması belirtilmemişse kullanılır.", + "site.og.headline": "Genel Açık Grafik (OG) Ayarları", + "site.og.headline.help": "Web sitenizin Facebook veya Twitter gibi sosyal ağlarda paylaşıldığında nasıl göründüğünü ayarlar.", + "fields.ogTitleTemplate.label": "Açık Grafik (OG) Başlık Şablonu", + "fields.ogDescription.label": "Açık Grafik (OG) Açıklaması", + "fields.ogSiteName.label": "Açık Grafik (OG) Site Adı", + "fields.ogImage.label": "Açık Grafik (OG) Görseli", + "fields.ogImage.help": "Önerilen boyut 1200x630 pikseldir.", + "fields.ogImage.empty": "Boş Açık Grafik (OG) Görseli", + "fields.cropOgImage.label": "OG görselini önerilen boyuta kırp?", + "fields.cropOgImage.help": "Önerilen boyut 1200x630px'dir. Etkinleştirildiğinde, sosyal medyada en iyi görüntü için görseller otomatik olarak bu boyuta kırpılacaktır.", + "fields.socialMediaAccounts.label": "Sosyal Medya Hesapları", + "fields.socialMediaAccounts.help": "Sosyal medya hesaplarınızın URL'leri veya @kullanici_adi. Meta verilerinde hesaplarınıza bağlantı eklemek için kullanılır.", + "page.meta.headline": "SEO Ayarları", + "page.og.headline": "Open Graph Ayarları", + "fields.titleOverwrite.label": "Başlık (üzerine yaz)", + "fields.inheritSettings.label": "Ayarları Miras Al", + "fields.inheritSettings.help": "Alt sayfalar tarafından miras alınacak ayarları seçin.\nÖrneğin, bir blogun tüm yazılarının sayfa varsayılanından farklı bir başlık şablonuna sahip olması gerekiyorsa bu yardımcı olabilir. Tüm ayarlar ana sayfada hala geçersiz kılınabilir.", + "fields.useTitleTemplate.label": "Başlık şablonunu kullan?", + "fields.useTitleTemplate.no": "Hayır - sadece başlık", + "fields.useTitleTemplate.yes": "Evet - şablonla", + "fields.useTitleTemplate.help": "Başlık şablonunun kullanılıp kullanılmayacağını belirtir. Miras alınmayacaktır.", + "writerNodes.template.title": "Sayfa başlığı", + "writerNodes.template.siteTitle": "Site başlığı", + "common.default": "Varsayılan:", + "common.yes": "Evet", + "common.no": "Hayır", + "sitemap.title": "Site Haritası", + "sitemap.index": "Site Haritası Dizini", + "sitemap.description": "Web sitenizdeki dizine eklenebilecek sayfalar hakkında arama motorlarını bilgilendiren web sitenizin site haritasıdır.", + "sitemap.by": "tarafından", + "sitemap.changefreq": "Değişim Sıklığı", + "sitemap.lastUpdated": "Son Güncelleme", + "sitemap.priority": "Öncelik", + "sitemap.url": "URL", + "sitemap.noEntries": "Giriş yok", + "utmShare.button": "Paylaş", + "utmShare.parameters": "Parametreler", + "utmShare.source.label": "Kaynak", + "utmShare.source.placeholder": "örn. google, bülten", + "utmShare.medium.label": "Ortam", + "utmShare.medium.placeholder": "örn. cpc, e-posta, sosyal medya", + "utmShare.campaign.label": "Kampanya", + "utmShare.campaign.placeholder": "örn. bahar_indirimi", + "utmShare.content.label": "İçerik", + "utmShare.content.placeholder": "örn. logo_linki", + "utmShare.term.label": "Terim", + "utmShare.term.placeholder": "örn. koşu ayakkabıları", + "utmShare.ref.label": "Ref", + "utmShare.ref.placeholder": "örn. getkirby.com", + "ai.action.generate": "Yapay zeka ile oluştur", + "ai.action.regenerate": "Yeniden oluştur", + "ai.action.edit": "Düzenle...", + "ai.action.customize": "Yapay zeka oluşturmayı özelleştir...", + "ai.action.stop": "Durdur", + "ai.error.request": "Yapay zeka isteği başarısız oldu. Lütfen tekrar dene.", + "ai.error.disabled": "Yapay zeka özellikleri devre dışı.", + "ai.error.permission": "Yapay zeka özelliklerini kullanma izniniz yok.", + "ai.dialog.instructions.label": "Talimatlar", + "ai.dialog.instructions.placeholder": "Metinde hangi değişiklikleri yapmak istiyorsun?", + "ai.dialog.edit.submit": "Değişiklikleri uygula", + "ai.dialog.custom.label": "Özel talimatlar", + "ai.dialog.custom.placeholder": "Ne tür bir içerik oluşturmak istiyorsun?", + "ai.dialog.custom.submit": "Oluştur", + "sections.searchConsole.title": "Google Search Console", + "sections.searchConsole.noCredentials": "Search Console'u bağlamak için Google Cloud kimlik bilgilerini ayarlayın. Kurulum talimatları için belgelere göz atın.", + "sections.searchConsole.notConnected": "Bu sayfa için arama verilerini görmek üzere Google hesabınızı bağlayın.", + "sections.searchConsole.selectProperty": "Hangi Search Console özelliğinin kullanılacağını seçin.", + "sections.searchConsole.selectPropertyButton": "Özellik seç", + "sections.searchConsole.selectPropertyLabel": "Özellik", + "sections.searchConsole.scDomain": "alan adı", + "sections.searchConsole.docs": "Belgeler", + "sections.searchConsole.connect": "Bağlan", + "sections.searchConsole.reconnect": "Yeniden bağlan", + "sections.searchConsole.noData": "Bu sayfa için kullanılabilir arama verisi yok.", + "sections.searchConsole.showMore": "Tümünü göster", + "sections.searchConsole.sortBy": "Sıralama ölçütü", + "sections.searchConsole.query": "Sorgu", + "sections.searchConsole.clicks": "Tıklamalar", + "sections.searchConsole.impressions": "Gösterimler", + "sections.searchConsole.ctr": "TO", + "sections.searchConsole.position": "Konum", + "sections.searchConsole.openInGsc": "Search Console'da aç" +} diff --git a/site/snippets/back-to-top.php b/site/snippets/back-to-top.php new file mode 100644 index 0000000..8868e95 --- /dev/null +++ b/site/snippets/back-to-top.php @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/snippets/card-open-graph.php b/site/snippets/card-open-graph.php index a88df84..9853bf8 100644 --- a/site/snippets/card-open-graph.php +++ b/site/snippets/card-open-graph.php @@ -80,6 +80,8 @@ ?>
+
+
<?= $ogData['title'] ?> @@ -105,4 +107,5 @@
+
\ No newline at end of file diff --git a/site/snippets/footer.php b/site/snippets/footer.php index 16332a7..0636217 100644 --- a/site/snippets/footer.php +++ b/site/snippets/footer.php @@ -1,8 +1,11 @@ - +
- + \ No newline at end of file diff --git a/site/snippets/header.php b/site/snippets/header.php index e035c5d..f932dc2 100644 --- a/site/snippets/header.php +++ b/site/snippets/header.php @@ -1,10 +1,11 @@ - + + <?php if ($page->isHomePage() == false): ?> @@ -13,7 +14,16 @@ <?= $site->title() ?> + + + + + + @@ -21,7 +31,7 @@

@@ -29,13 +39,13 @@

- -

L’exécution de Nidal et Khaled ‘Amirah à Naplouse

+ +

L’exécution de Nidal et Khaled ‘Amirah à Naplouse

@@ -49,8 +59,14 @@
-
diff --git a/site/snippets/modal-share.php b/site/snippets/modal-share.php index 888f0a0..e0a4037 100644 --- a/site/snippets/modal-share.php +++ b/site/snippets/modal-share.php @@ -1,8 +1,13 @@