Compare commits
121 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f9e75126e | ||
|
|
df2843123f | ||
|
|
78a63a9d60 | ||
|
|
e8e23379b0 | ||
|
|
bf20f1d394 | ||
|
|
772d310fa9 | ||
|
|
cba733c14c | ||
|
|
6a841d3b57 | ||
|
|
5d63958374 | ||
|
|
b019b99284 | ||
|
|
9f65afaf70 | ||
|
|
bb924bed7b | ||
|
|
9d3800690d | ||
|
|
7f661597ba | ||
|
|
a1d945a8ab | ||
|
|
18db74998d | ||
|
|
28fc768228 | ||
|
|
5496ae3964 | ||
|
|
b6a573e7ad | ||
|
|
6c245966e1 | ||
|
|
455801a0d2 | ||
|
|
217dde5842 | ||
|
|
949d7437a4 | ||
|
|
328fd95842 | ||
|
|
915f3e91b5 | ||
|
|
dc3750ffb7 | ||
|
|
205dac3fde | ||
|
|
f951d3e5d6 | ||
|
|
a5d8f4dc80 | ||
|
|
0766e64b96 | ||
|
|
b4217f80d4 | ||
|
|
08c2f57d6e | ||
|
|
8629d58240 | ||
|
|
57edf14f53 | ||
|
|
60709ab632 | ||
|
|
7f4eb816c1 | ||
|
|
23153263a0 | ||
|
|
be447667cc | ||
|
|
72a84c9283 | ||
|
|
4758048e61 | ||
|
|
d76a53a3f6 | ||
|
|
21e058b336 | ||
|
|
025b3a1d08 | ||
|
|
ff2b7663a2 | ||
|
|
1dad30e71a | ||
|
|
014858fe82 | ||
|
|
d5d0b29fb0 | ||
|
|
989f042158 | ||
|
|
b525ea4f5c | ||
|
|
c053ce2d27 | ||
|
|
2e718f61ae | ||
|
|
bd2503fb52 | ||
|
|
62c13dccee | ||
|
|
dd4be3e2fb | ||
|
|
1cd5360a1a | ||
|
|
f903d12369 | ||
|
|
d54ed28aa0 | ||
|
|
3a6ff118bc | ||
|
|
3dd3286bb8 | ||
|
|
4f6e32bfaa | ||
|
|
08323f4a02 | ||
|
|
b6df7adc6a | ||
|
|
cb5e08d3fc | ||
|
|
04b04ccf6a | ||
|
|
9b9e5a2ef7 | ||
|
|
dde483ee20 | ||
|
|
669a35daaf | ||
|
|
0e7fa10ab3 | ||
|
|
5f3a577c1c | ||
|
|
f76e3d9fb3 | ||
|
|
72fff85a8c | ||
|
|
2ad2c593ce | ||
|
|
f095954a33 | ||
|
|
8bdd63afe2 | ||
|
|
416d8e94b3 | ||
|
|
b2853043c8 | ||
|
|
7d577654a4 | ||
|
|
be631dbe67 | ||
|
|
84a8437ad2 | ||
|
|
fc267d80d1 | ||
|
|
1e91353c86 | ||
|
|
3fd93ac292 | ||
|
|
172923fc6d | ||
|
|
1c2c45e5ab | ||
|
|
4cdb92017b | ||
|
|
4172bba4cc | ||
|
|
979106e8eb | ||
|
|
69f4cddd00 | ||
|
|
5d927217f9 | ||
|
|
5d09b519c3 | ||
|
|
32b81867e5 | ||
|
|
c4048f8a60 | ||
|
|
9d3d534de3 | ||
|
|
40685d633b | ||
|
|
2af1980274 | ||
|
|
cc3feb0366 | ||
|
|
f1e85efcf9 | ||
|
|
aa237d8628 | ||
|
|
b1923e8d4c | ||
|
|
afd3df123f | ||
|
|
fe4df9cbd3 | ||
|
|
49d48015a8 | ||
|
|
7e5872aad1 | ||
|
|
5c467757fe | ||
|
|
b1418ef194 | ||
|
|
aea050f6cc | ||
|
|
6801286c5f | ||
|
|
e3829df88c | ||
|
|
fdd1650343 | ||
|
|
affda01429 | ||
|
|
9aee1ad5b5 | ||
|
|
7cda66f837 | ||
|
|
cab0bb1ca4 | ||
|
|
5252d51633 | ||
|
|
af1ffe78c6 | ||
|
|
9b1aeb5cd0 | ||
|
|
1b62fa7589 | ||
|
|
4c5b7677d7 | ||
|
|
8e09bceb02 | ||
|
|
e3207ee58e | ||
|
|
6f3774a5d7 |
141 changed files with 14643 additions and 1357 deletions
BIN
apple-touch-icon.png
Normal file
BIN
apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.9 KiB |
|
|
@ -8,3 +8,18 @@
|
|||
.k-panel-menu-button[aria-current] * {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.k-panel[data-template="year"] .k-list-items .k-item:first-child {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.k-panel[data-template="year"] .k-list-items::before {
|
||||
content: "Texte princeps";
|
||||
display: block;
|
||||
padding: 0.5rem;
|
||||
padding-bottom: 0.7rem;
|
||||
border-radius: var(--rounded-md) var(--rounded-md) 0 0;
|
||||
transform: translateY(0.3rem);
|
||||
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,39 @@
|
|||
article #main-content {
|
||||
max-width: calc(18 * var(--unit--horizontal));
|
||||
scroll-margin-block-start: calc(var(--unit--vertical) * 6);
|
||||
margin-top: calc(var(--unit--vertical) * 2);
|
||||
padding-bottom: calc(var(--unit--vertical) * 2);
|
||||
margin-top: calc(2 * var(--unit--vertical));
|
||||
}
|
||||
|
||||
#main-content .texts {
|
||||
margin-top: calc(2 * var(--unit--vertical));
|
||||
}
|
||||
|
||||
#main-content .see-more {
|
||||
margin-top: calc(var(--unit--vertical) * 0.5);
|
||||
}
|
||||
|
||||
[data-template="year"] article > h1 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
article #main-content #chapo::after {
|
||||
content: "";
|
||||
display: block;
|
||||
margin-top: calc(2 * var(--unit--vertical));
|
||||
margin-bottom: calc(4 * var(--unit--vertical));
|
||||
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
article #main-content li:not(.text) {
|
||||
list-style-type: inherit;
|
||||
}
|
||||
|
||||
article h3 {
|
||||
margin-top: calc(3 * var(--unit--vertical));
|
||||
margin-bottom: calc(1 * var(--unit--vertical));
|
||||
}
|
||||
|
||||
@media screen and (min-width: 640px) {
|
||||
|
|
|
|||
|
|
@ -1,36 +1,72 @@
|
|||
#main-footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: var(--unit--horizontal);
|
||||
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
[data-template="home"] #main-footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#main-footer ul {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: calc(2 * var(--unit--horizontal));
|
||||
#main-footer li:not(.open-nav-wrapper) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#main-footer a {
|
||||
text-shadow: 0 0 2px #000;
|
||||
-moz-text-shadow: 0 0 2px #000;
|
||||
-webkit-text-shadow: 0 0 2px #000;
|
||||
#main-footer button.open-nav {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
[data-template="home"] .title-wrapper button.open-nav {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
#main-footer .open-nav {
|
||||
box-sizing: border-box;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
border-top: 1px solid #fff;
|
||||
font-size: var(--font-size-m);
|
||||
background-color: #000;
|
||||
padding: calc(var(--unit--vertical) / 2) var(--unit--horizontal);
|
||||
margin-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 640px) {
|
||||
#main-footer li {
|
||||
display: block !important;
|
||||
}
|
||||
#main-footer {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: var(--body-padding);
|
||||
background-color: transparent;
|
||||
padding: var(--unit--vertical) var(--unit--horizontal);
|
||||
}
|
||||
#main-footer ul {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#main-footer button.open-nav {
|
||||
margin-bottom: var(--unit--vertical);
|
||||
}
|
||||
|
||||
[data-template="home"] #main-footer .open-nav-wrapper {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.open-nav-wrapper {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,18 @@ body {
|
|||
opacity: var(--opacity-light);
|
||||
}
|
||||
|
||||
.footnote::before {
|
||||
content: "[";
|
||||
}
|
||||
.footnote::after {
|
||||
content: "]";
|
||||
}
|
||||
|
||||
/* ================= COLORS ================= */
|
||||
.texts .text {
|
||||
margin-bottom: var(--unit--vertical);
|
||||
}
|
||||
|
||||
/* ================= COLORS ================= */
|
||||
.color {
|
||||
color: var(--color);
|
||||
|
|
@ -84,7 +96,8 @@ body {
|
|||
|
||||
/* ================= BUTTONS ================= */
|
||||
.toggle-btn--left::after,
|
||||
button.plus::after {
|
||||
button.plus::after,
|
||||
button.less::after {
|
||||
margin-left: var(--unit--horizontal);
|
||||
}
|
||||
|
||||
|
|
@ -92,7 +105,8 @@ button.plus::after {
|
|||
button.plus::after {
|
||||
content: "+";
|
||||
}
|
||||
.toggle-btn--left.open::after {
|
||||
.toggle-btn--left.open::after,
|
||||
button.less::after {
|
||||
content: "-";
|
||||
}
|
||||
.toggle-btn--right::before {
|
||||
|
|
@ -108,3 +122,19 @@ button.plus::after {
|
|||
.transition {
|
||||
transition: all 0.5s var(--curve-sine);
|
||||
}
|
||||
|
||||
.short {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 5;
|
||||
overflow: hidden;
|
||||
|
||||
/* Do not remove : required not to crop letters descenders */
|
||||
padding-bottom: 0.16rem;
|
||||
height: 8.5rem;
|
||||
}
|
||||
|
||||
.see-more {
|
||||
width: 100%;
|
||||
margin-top: calc(var(--unit--vertical) / 2);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,13 +13,6 @@ body.full-width #main-content {
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(39, 1fr);
|
||||
column-gap: var(--unit--horizontal);
|
||||
margin-bottom: calc(var(--unit--vertical) * 2);
|
||||
}
|
||||
|
||||
.grid .column {
|
||||
grid-column: span var(--span);
|
||||
}
|
||||
|
|
@ -27,3 +20,26 @@ body.full-width #main-content {
|
|||
.grid[data-columns="1"] .column {
|
||||
grid-column: 11 / span 20;
|
||||
}
|
||||
|
||||
[data-template="grid"] main #main-content {
|
||||
margin-left: inherit;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 640px) {
|
||||
.grid .column:not(:last-child) {
|
||||
margin-bottom: calc(2 * var(--unit--vertical));
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 640px) {
|
||||
[data-template="grid"] main #main-content {
|
||||
margin-left: calc(0px - calc(4 * var(--unit--horizontal)));
|
||||
}
|
||||
|
||||
[data-template="grid"] .grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(39, 1fr);
|
||||
column-gap: var(--unit--horizontal);
|
||||
margin-bottom: calc(var(--unit--vertical) * 2);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,29 @@
|
|||
main {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
main article > div {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.page-cover > * {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
#main-header {
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
box-sizing: border-box;
|
||||
width: 100vw;
|
||||
padding-top: calc(var(--unit--vertical) / 2);
|
||||
}
|
||||
|
||||
[data-template="home"] #main-header {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#logo * {
|
||||
font-size: 26.65vw;
|
||||
font-size: 25.3vw;
|
||||
font-weight: var(--font-weight-extra-bold);
|
||||
}
|
||||
|
||||
|
|
@ -14,7 +31,7 @@
|
|||
height: 20vw;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding-right: 1vw;
|
||||
padding-right: 3vw;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
|
|
@ -24,6 +41,10 @@
|
|||
text-align: right;
|
||||
}
|
||||
|
||||
#logo #actuel {
|
||||
font-weight: 550;
|
||||
}
|
||||
|
||||
#logo #actuel,
|
||||
#logo #inactuel {
|
||||
mix-blend-mode: difference;
|
||||
|
|
@ -35,82 +56,91 @@
|
|||
|
||||
#main-header.minimized #inactuel {
|
||||
margin-top: -20vw;
|
||||
transform: translateX(-2px) translateY(-2px);
|
||||
transform: translateX(-1px) translateY(-1px);
|
||||
}
|
||||
|
||||
#main-header.minimized #inactuel:not([data-template="home"] *) {
|
||||
transform: translateX(-1px) translateY(-1px) !important;
|
||||
}
|
||||
|
||||
.page-cover,
|
||||
article > h1 {
|
||||
padding-top: calc(50vw);
|
||||
}
|
||||
|
||||
.page-cover {
|
||||
position: relative;
|
||||
height: 100svh;
|
||||
box-sizing: border-box;
|
||||
padding-top: calc(var(--unit--vertical-relative) * 9);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[data-template="home"] .page-cover {
|
||||
padding-top: calc(42.5vw);
|
||||
}
|
||||
|
||||
.page-cover .title-wrapper h1 {
|
||||
margin-bottom: calc(0.255 * var(--unit--vertical));
|
||||
}
|
||||
|
||||
.page-cover .text-wrapper {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* ================= ENTRY BTNS ================= */
|
||||
#entry-btns {
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: calc(var(--unit--vertical) * 4);
|
||||
height: var(--entry-btns-height);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
[data-template="info"] #entry-btns {
|
||||
top: calc(var(--unit--vertical) * 4);
|
||||
}
|
||||
|
||||
[data-template="home"] .entry-btn {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
button.toggle.left::after,
|
||||
button.toggle.right::before {
|
||||
transition: all 0.5s var(--curve-sine);
|
||||
content: "+";
|
||||
}
|
||||
|
||||
#entry-btns.minimized {
|
||||
color: #000;
|
||||
}
|
||||
#entry-btns.minimized .entry-btn--left::before,
|
||||
#entry-btns.minimized .entry-btn--right::after {
|
||||
background-color: var(--color-secondary);
|
||||
.page-cover .links {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#entry-btns.minimized .entry-btn::before,
|
||||
#entry-btns.minimized .entry-btn::after {
|
||||
font-weight: bold;
|
||||
.page-cover .links li {
|
||||
display: inline-block;
|
||||
margin-right: var(--unit--horizontal);
|
||||
}
|
||||
|
||||
#entry-btns.minimized .entry-btn--left {
|
||||
padding-right: 4px;
|
||||
margin-left: calc(-4px - var(--width));
|
||||
[data-template="author"] .page-cover,
|
||||
[data-template="category"] .page-cover,
|
||||
[data-template="year"] .page-cover,
|
||||
[data-template="email"] .page-cover,
|
||||
[data-template="error"] .page-cover,
|
||||
[data-template="info"] .page-cover {
|
||||
height: initial !important;
|
||||
}
|
||||
#entry-btns.minimized .entry-btn--right {
|
||||
padding-left: 4px;
|
||||
margin-right: calc(-4px - var(--width));
|
||||
[data-template="author"] .page-cover .links,
|
||||
[data-template="category"] .page-cover .links,
|
||||
[data-template="year"] .page-cover .links,
|
||||
[data-template="email"] .page-cover .links,
|
||||
[data-template="error"] .page-cover .links,
|
||||
[data-template="info"] .page-cover .links {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 640px) {
|
||||
body:not([data-template="home"]) #main-header {
|
||||
width: var(--body-padding);
|
||||
}
|
||||
#logo * {
|
||||
font-size: 26.65vw;
|
||||
}
|
||||
#logo span {
|
||||
padding-right: 1vw;
|
||||
}
|
||||
body:not([data-template="home"]) #logo * {
|
||||
font-size: 6vw;
|
||||
font-size: 5.6vw;
|
||||
}
|
||||
body:not([data-template="home"]) #logo span {
|
||||
height: 5vw;
|
||||
padding-right: calc(var(--unit--horizontal) - 0.3vw);
|
||||
}
|
||||
body:not([data-template="home"]) #main-header.minimized #inactuel {
|
||||
margin-top: -4.9vw;
|
||||
|
|
@ -123,20 +153,21 @@ button.toggle.right::before {
|
|||
[data-template="category"] .page-cover
|
||||
) {
|
||||
height: 100vh;
|
||||
padding: calc(10 * var(--unit--vertical)) 0;
|
||||
padding-top: calc(var(--unit--vertical) * 8);
|
||||
}
|
||||
|
||||
.page-cover,
|
||||
article > h1 {
|
||||
padding-top: 15vw;
|
||||
}
|
||||
.page-cover {
|
||||
min-height: calc(22 * var(--unit--vertical));
|
||||
}
|
||||
|
||||
[data-template="home"] .page-cover {
|
||||
padding-top: calc(42.5vw) !important;
|
||||
}
|
||||
|
||||
[data-template="author"] .page-cover,
|
||||
[data-template="category"] .page-cover,
|
||||
[data-template="year"] .page-cover {
|
||||
height: initial;
|
||||
}
|
||||
|
||||
#entry-btns {
|
||||
.page-cover .links {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1,9 @@
|
|||
.main-edito-btn {
|
||||
display: inline-block;
|
||||
margin-right: calc(2 * var(--unit--horizontal));
|
||||
}
|
||||
|
||||
#main-edito {
|
||||
margin-top: calc(var(--unit--vertical) * 4);
|
||||
scroll-margin-block-start: 25vw;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,11 @@ body {
|
|||
|
||||
main {
|
||||
padding: 0 var(--unit--horizontal);
|
||||
padding-bottom: calc(2 * var(--unit--vertical)) !important;
|
||||
}
|
||||
[data-template="info"] main {
|
||||
/* [data-template="info"] main {
|
||||
margin-top: calc(var(--unit--vertical) * 2);
|
||||
}
|
||||
} */
|
||||
|
||||
hr {
|
||||
height: 1px;
|
||||
|
|
@ -38,7 +39,7 @@ html {
|
|||
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 255, 255, 0.25) transparent;
|
||||
scrollbar-color: rgba(255, 255, 255) transparent;
|
||||
}
|
||||
|
||||
/* Works on Chrome, Edge, and Safari */
|
||||
|
|
@ -53,7 +54,7 @@ html {
|
|||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
background-color: rgba(255, 255, 255);
|
||||
border-radius: 0px;
|
||||
border: none;
|
||||
}
|
||||
|
|
@ -69,6 +70,7 @@ html {
|
|||
}
|
||||
|
||||
main {
|
||||
padding: 0 var(--body-padding);
|
||||
width: min(60vw, 45rem);
|
||||
padding-left: var(--body-padding);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
/* ================= PANELS ================= */
|
||||
#nav-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
|
|
@ -105,6 +104,9 @@ button.search__icon {
|
|||
padding: var(--unit--vertical) var(--unit--horizontal);
|
||||
}
|
||||
|
||||
footer {
|
||||
width: 100%;
|
||||
}
|
||||
.panel-close {
|
||||
position: fixed;
|
||||
box-sizing: border-box;
|
||||
|
|
@ -134,15 +136,15 @@ button.search__icon {
|
|||
}
|
||||
|
||||
/* ================= YEARS ================= */
|
||||
.panel__collection .panel__item:last-child {
|
||||
margin-bottom: 6rem;
|
||||
}
|
||||
.panel-item-content__edito {
|
||||
margin-bottom: calc(var(--unit--vertical) / 2);
|
||||
}
|
||||
|
||||
.panel-item-content__edito.short {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 5;
|
||||
overflow: hidden;
|
||||
.panel-item-content__edito p:not(:last-child) {
|
||||
margin-bottom: var(--unit--vertical);
|
||||
}
|
||||
|
||||
button.see-more {
|
||||
|
|
@ -154,43 +156,37 @@ button.see-more {
|
|||
}
|
||||
|
||||
/* ================= TEXT ITEM ================= */
|
||||
.text {
|
||||
margin-bottom: var(--unit--vertical);
|
||||
.panel .text:first-child,
|
||||
.panel .text:last-child {
|
||||
margin-bottom: calc(var(--unit--vertical) * 2);
|
||||
}
|
||||
|
||||
.panel__collection--years .text:first-child .text__title {
|
||||
display: inline-block;
|
||||
padding-left: var(--unit--horizontal);
|
||||
}
|
||||
|
||||
.panel .text__subtitle {
|
||||
margin-bottom: calc(var(--unit--vertical) / 4);
|
||||
}
|
||||
|
||||
.panel__collection--years .text:first-child .text__infos {
|
||||
padding-left: var(--unit--horizontal);
|
||||
}
|
||||
.panel__collection--years .text:first-child .text__infos::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 640px) {
|
||||
nav.panel {
|
||||
width: 40rem;
|
||||
}
|
||||
#subscribe-btn-wrapper {
|
||||
height: 2.5rem;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
#subscribe-form {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
#subscribe-form input {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-bottom: 1px solid #fff;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 0.3rem;
|
||||
padding-right: 2rem;
|
||||
color: #fff;
|
||||
width: 15rem;
|
||||
}
|
||||
#subscribe-form input:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
#subscribe-form button[type="submit"] {
|
||||
color: #fff;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.panel {
|
||||
z-index: 4;
|
||||
|
|
|
|||
29
assets/css/src/newsletter.css
Normal file
29
assets/css/src/newsletter.css
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#subscribe-btn-wrapper {
|
||||
height: 2.5rem;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
#subscribe-form {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
#subscribe-form input {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-bottom: 1px solid #fff;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 0.3rem;
|
||||
padding-right: 2rem;
|
||||
color: #fff;
|
||||
width: 15rem;
|
||||
}
|
||||
#subscribe-form input:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
#subscribe-form button[type="submit"] {
|
||||
color: #fff;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
|
@ -76,18 +76,6 @@ button.toggle.right.open::before {
|
|||
border: none;
|
||||
}
|
||||
|
||||
.texts__year.short .year__edito {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 5;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.texts__year .see-more {
|
||||
width: 100%;
|
||||
margin-top: calc(var(--unit--vertical) / 2);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 640px) {
|
||||
[data-template="home"] #tabs {
|
||||
margin-top: calc(0px - (10 * var(--unit--vertical)));
|
||||
|
|
|
|||
|
|
@ -28,12 +28,12 @@ h4 *,
|
|||
h5,
|
||||
h5 *,
|
||||
p,
|
||||
p * {
|
||||
p *:not(strong) {
|
||||
font-weight: var(--font-weight-light);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
h2 {
|
||||
h1 {
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
|
||||
|
|
@ -87,11 +87,11 @@ button,
|
|||
}
|
||||
.fs-l {
|
||||
font-size: var(--font-size-l) !important;
|
||||
line-height: var(--unit--vertical);
|
||||
line-height: calc(var(--unit--vertical) * 1.3);
|
||||
}
|
||||
.fs-xl {
|
||||
font-size: var(--font-size-xl) !important;
|
||||
line-height: calc(var(--unit--vertical) * 1.5) !important;
|
||||
line-height: calc(var(--unit--vertical) * 2) !important;
|
||||
}
|
||||
.fs-xxl {
|
||||
font-size: var(--font-size-xxl) !important;
|
||||
|
|
@ -137,6 +137,14 @@ button {
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
a {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
a > * {
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
a * {
|
||||
transition: font 0.2s ease-in-out;
|
||||
}
|
||||
|
|
@ -166,14 +174,29 @@ article p:not(:last-child) {
|
|||
|
||||
.footnote,
|
||||
.footnote * {
|
||||
font-style: normal !important;
|
||||
scroll-margin-block-start: calc(var(--unit--vertical) * 6);
|
||||
color: var(--color-secondary);
|
||||
color: var(--color-primary);
|
||||
text-decoration: none !important;
|
||||
font-weight: var(--font-weight-bold) !important;
|
||||
}
|
||||
|
||||
.footnote sup {
|
||||
all: unset;
|
||||
}
|
||||
|
||||
.footnote:focus-visible,
|
||||
.footnote:target {
|
||||
text-decoration: underline !important;
|
||||
text-underline-offset: 0.5rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 640px) {
|
||||
.title-center {
|
||||
margin-left: calc(2 * var(--unit--horizontal));
|
||||
}
|
||||
|
||||
.fs-l {
|
||||
line-height: calc(var(--unit--vertical) * 1.5);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
@import url("src/virtual.css");
|
||||
@import url("src/home.css");
|
||||
@import url("src/grid.css");
|
||||
@import url("src/newsletter.css");
|
||||
@import url("src/footer.css");
|
||||
@import url("src/print.css");
|
||||
:root {
|
||||
|
|
|
|||
202
assets/dist/script.js
vendored
202
assets/dist/script.js
vendored
|
|
@ -1,63 +1,181 @@
|
|||
"use strict";
|
||||
|
||||
var remFactor = 16;
|
||||
var verticalUnit = 1.3 * remFactor;
|
||||
function toggleTab(data, tab) {
|
||||
if (data.activeTab === tab) {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: "smooth"
|
||||
});
|
||||
setTimeout(function () {
|
||||
data.isOpen = false;
|
||||
data.activeTab = "";
|
||||
}, 500);
|
||||
} else {
|
||||
data.activeTab = tab;
|
||||
data.isOpen = true;
|
||||
scrollToElem(".active-tab");
|
||||
}
|
||||
var verticalUnit = getUnit("--unit--vertical");
|
||||
function getUnit(id) {
|
||||
var remFactor = 16;
|
||||
var rawUnit = getComputedStyle(document.documentElement).getPropertyValue(id) || "1.7rem";
|
||||
var remUnit = parseFloat(rawUnit);
|
||||
var pxUnit = remUnit * remFactor;
|
||||
return pxUnit;
|
||||
}
|
||||
function scrollToElem(selector) {
|
||||
document.querySelector(".active-tab").scrollTop = 0;
|
||||
setTimeout(function () {
|
||||
var yOffset = -7 * verticalUnit;
|
||||
var elem = document.querySelector(selector);
|
||||
var top = elem.getBoundingClientRect().top;
|
||||
window.scrollTo({
|
||||
top: top + window.scrollY + yOffset,
|
||||
behavior: "smooth"
|
||||
});
|
||||
}, 100);
|
||||
|
||||
// Throttle found here : https://gist.github.com/ionurboz/51b505ee3281cd713747b4a84d69f434
|
||||
function throttle(func, wait, options) {
|
||||
var context, args, result;
|
||||
var timeout = null;
|
||||
var previous = 0;
|
||||
if (!options) options = {};
|
||||
var later = function later() {
|
||||
previous = options.leading === false ? 0 : Date.now();
|
||||
timeout = null;
|
||||
result = func.apply(context, args);
|
||||
if (!timeout) context = args = null;
|
||||
};
|
||||
return function () {
|
||||
var now = Date.now();
|
||||
if (!previous && options.leading === false) previous = now;
|
||||
var remaining = wait - (now - previous);
|
||||
context = this;
|
||||
args = arguments;
|
||||
if (remaining <= 0 || remaining > wait) {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
previous = now;
|
||||
result = func.apply(context, args);
|
||||
if (!timeout) context = args = null;
|
||||
} else if (!timeout && options.trailing !== false) {
|
||||
timeout = setTimeout(later, remaining);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
function setWindowHeightFactor() {
|
||||
var windowHeight = window.innerHeight;
|
||||
var min = 650;
|
||||
var delta = windowHeight - min;
|
||||
var factor = roundToNearestHalf(delta / 300) + 1;
|
||||
var head = document.querySelector("head");
|
||||
var style = document.createElement("style");
|
||||
style.innerText = ":root { --window-height-factor:".concat(factor, " }");
|
||||
head.appendChild(style);
|
||||
document.querySelector(":root").style.setProperty("--window-height-factor", factor);
|
||||
}
|
||||
function roundToNearestHalf(num) {
|
||||
var round = Math.round(num * 2) / 2;
|
||||
return Math.max(round, 0);
|
||||
}
|
||||
setWindowHeightFactor();
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
function toggleLogoState() {
|
||||
var scrollY = window.scrollY || window.pageYOffset;
|
||||
if (scrollY > 10) {
|
||||
document.querySelector("#main-header").classList.add("minimized");
|
||||
} else {
|
||||
document.querySelector("#main-header").classList.remove("minimized");
|
||||
}
|
||||
function toggleLogoState() {
|
||||
var scrollY = window.scrollY || window.pageYOffset;
|
||||
if (scrollY > 10) {
|
||||
document.querySelector("#main-header").classList.add("minimized");
|
||||
} else {
|
||||
document.querySelector("#main-header").classList.remove("minimized");
|
||||
}
|
||||
}
|
||||
function toggleFooterState() {
|
||||
if (scrollY > 90) {
|
||||
document.querySelector("#main-footer").classList.add("main-footer--background");
|
||||
} else {
|
||||
document.querySelector("#main-footer").classList.remove("main-footer--background");
|
||||
}
|
||||
}
|
||||
function fixFootNotes() {
|
||||
var footnotes = document.querySelectorAll('a[href^="#sdfootnote"]');
|
||||
footnotes.forEach(function (footnote) {
|
||||
var href = footnote.href;
|
||||
footnote.classList.add("footnote");
|
||||
if (href.includes("sym")) {
|
||||
footnote.id = footnote.hash.replace("sym", "anc").replace("#", "");
|
||||
} else if (href.includes("anc")) {
|
||||
footnote.id = footnote.hash.replace("anc", "sym").replace("#", "");
|
||||
}
|
||||
});
|
||||
}
|
||||
function removeAccents(str) {
|
||||
var from = "áäâàãåčçćďéěëèêẽĕȇíìîïňñóöòôõøðřŕšťúůüùûýÿžþÞĐđ߯a·/_,:;";
|
||||
var to = "aaaaaacccdeeeeeeeeiiiinnooooooorrstuuuuuyyzbBDdBAa------";
|
||||
for (var i = 0, l = from.length; i < l; i++) {
|
||||
str = str.replace(new RegExp(from.charAt(i), "g"), to.charAt(i));
|
||||
}
|
||||
return str;
|
||||
}
|
||||
function slugify(str) {
|
||||
return removeAccents(str.toLowerCase());
|
||||
}
|
||||
function subscribe(event) {
|
||||
event.preventDefault();
|
||||
var emailInput = document.querySelector("#subscribe-form input");
|
||||
if (emailInput.value.toLowerCase().match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
|
||||
var header = {
|
||||
method: "POST",
|
||||
body: JSON.stringify(emailInput.value)
|
||||
};
|
||||
fetch("/subscribe.json", header).then(function (res) {
|
||||
return res.json();
|
||||
}).then(function (data) {
|
||||
var formNode = emailInput.parentNode.parentNode;
|
||||
formNode.outerHTML = "<p>" + data.message + "</p>";
|
||||
});
|
||||
} else {
|
||||
emailInput.value = "E-mail invalide. Recommencez.";
|
||||
}
|
||||
}
|
||||
var panelNav = document.querySelector(".panel");
|
||||
var navOverlay = document.querySelector("#nav-overlay");
|
||||
var openNavBtn = document.querySelector("button.open-nav");
|
||||
var closeNavBtn = document.querySelector(".panel-close");
|
||||
function closeNav() {
|
||||
panelNav.classList.remove("panel--visible");
|
||||
navOverlay.classList.remove("nav-overlay--visible");
|
||||
document.body.classList.remove("no-scroll");
|
||||
}
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
ragadjust("h1, h2, h4, h5", ["all"]);
|
||||
window.window.scrollTo({
|
||||
top: 0
|
||||
});
|
||||
window.addEventListener("scroll", function () {
|
||||
var handleScroll = throttle(function () {
|
||||
toggleLogoState();
|
||||
if (window.innerWidth <= 680) {
|
||||
toggleFooterState();
|
||||
}
|
||||
}, 100);
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
setWindowHeightFactor();
|
||||
window.addEventListener("resize", function () {
|
||||
setWindowHeightFactor();
|
||||
});
|
||||
fixFootNotes();
|
||||
window.addEventListener("keyup", function (event) {
|
||||
if (event.key === "Escape") {
|
||||
closeNav();
|
||||
}
|
||||
});
|
||||
document.querySelectorAll(".panel").forEach(function (panel) {
|
||||
panel.addEventListener("click", function (event) {
|
||||
event.stopPropagation();
|
||||
});
|
||||
});
|
||||
var navSortBtns = document.querySelectorAll("nav .sort-btn");
|
||||
var navSections = document.querySelectorAll(".panel__all-texts, .panel__collection");
|
||||
navSortBtns.forEach(function (sortBtn) {
|
||||
sortBtn.addEventListener("click", function () {
|
||||
navSortBtns.forEach(function (btn) {
|
||||
return btn.classList.remove("active");
|
||||
});
|
||||
sortBtn.classList.add("active");
|
||||
var sections = {
|
||||
"sort-btn--all": ".panel__all-texts",
|
||||
"sort-btn--years": ".panel__collection--years",
|
||||
"sort-btn--categories": ".panel__collection--categories"
|
||||
};
|
||||
navSections.forEach(function (navSection) {
|
||||
return navSection.classList.add("hidden");
|
||||
});
|
||||
Object.keys(sections).forEach(function (key) {
|
||||
if (sortBtn.classList.contains(key)) {
|
||||
document.querySelector(sections[key]).classList.remove("hidden");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
openNavBtn.addEventListener("click", function () {
|
||||
panelNav.classList.add("panel--visible");
|
||||
navOverlay.classList.add("nav-overlay--visible");
|
||||
document.body.classList.add("no-scroll");
|
||||
});
|
||||
closeNavBtn.addEventListener("click", function () {
|
||||
closeNav();
|
||||
});
|
||||
navOverlay.addEventListener("click", function () {
|
||||
closeNav();
|
||||
});
|
||||
});
|
||||
475
assets/dist/style.css
vendored
475
assets/dist/style.css
vendored
File diff suppressed because one or more lines are too long
|
|
@ -9,16 +9,36 @@ function getUnit(id) {
|
|||
return pxUnit;
|
||||
}
|
||||
|
||||
function throttle(callback, limit) {
|
||||
let waiting = false;
|
||||
// Throttle found here : https://gist.github.com/ionurboz/51b505ee3281cd713747b4a84d69f434
|
||||
function throttle(func, wait, options) {
|
||||
var context, args, result;
|
||||
var timeout = null;
|
||||
var previous = 0;
|
||||
if (!options) options = {};
|
||||
var later = function () {
|
||||
previous = options.leading === false ? 0 : Date.now();
|
||||
timeout = null;
|
||||
result = func.apply(context, args);
|
||||
if (!timeout) context = args = null;
|
||||
};
|
||||
return function () {
|
||||
if (!waiting) {
|
||||
callback.apply(this, arguments);
|
||||
waiting = true;
|
||||
setTimeout(function () {
|
||||
waiting = false;
|
||||
}, limit);
|
||||
var now = Date.now();
|
||||
if (!previous && options.leading === false) previous = now;
|
||||
var remaining = wait - (now - previous);
|
||||
context = this;
|
||||
args = arguments;
|
||||
if (remaining <= 0 || remaining > wait) {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
previous = now;
|
||||
result = func.apply(context, args);
|
||||
if (!timeout) context = args = null;
|
||||
} else if (!timeout && options.trailing !== false) {
|
||||
timeout = setTimeout(later, remaining);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -47,17 +67,25 @@ function toggleLogoState() {
|
|||
document.querySelector("#main-header").classList.remove("minimized");
|
||||
}
|
||||
}
|
||||
function toggleFooterState() {
|
||||
if (scrollY > 90) {
|
||||
document.querySelector(".open-nav-wrapper").classList.remove("hidden");
|
||||
} else {
|
||||
document.querySelector(".open-nav-wrapper").classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function fixFootNotes() {
|
||||
const footnotes = document.querySelectorAll('a[href^="#sdfootnote"]');
|
||||
|
||||
footnotes.forEach((footnote) => {
|
||||
const href = footnote.href;
|
||||
|
||||
footnote.classList.add("footnote");
|
||||
|
||||
if (href.includes("sym")) {
|
||||
footnote.id = footnote.hash.replace("sym", "anc").replace("#", "");
|
||||
}
|
||||
if (href.includes("anc")) {
|
||||
} else if (href.includes("anc")) {
|
||||
footnote.id = footnote.hash.replace("anc", "sym").replace("#", "");
|
||||
}
|
||||
});
|
||||
|
|
@ -76,38 +104,30 @@ function slugify(str) {
|
|||
return removeAccents(str.toLowerCase());
|
||||
}
|
||||
|
||||
const subscribeBtn = document.querySelector("#subscribe-btn");
|
||||
function showSubscribeField(event) {
|
||||
event.preventDefault();
|
||||
const button = event.target;
|
||||
const li = button.parentNode;
|
||||
const form = li.querySelector("#subscribe-form");
|
||||
const input = form.querySelector("input");
|
||||
|
||||
form.classList.remove("hidden");
|
||||
button.classList.add("hidden");
|
||||
input.focus();
|
||||
}
|
||||
|
||||
function subscribe(event) {
|
||||
event.preventDefault();
|
||||
const email = document.querySelector("#subscribe-form input");
|
||||
const emailInput = document.querySelector("#subscribe-form input");
|
||||
|
||||
if (email.value.toLowerCase().match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
|
||||
if (emailInput.value.toLowerCase().match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
|
||||
const header = {
|
||||
method: "POST",
|
||||
body: email.value,
|
||||
body: JSON.stringify(emailInput.value),
|
||||
};
|
||||
|
||||
fetch("/subscribe.json");
|
||||
fetch("/subscribe.json", header)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
const formNode = emailInput.parentNode.parentNode;
|
||||
formNode.outerHTML = "<p>" + data.message + "</p>";
|
||||
});
|
||||
} else {
|
||||
email.value = "E-mail invalide. Recommencez.";
|
||||
emailInput.value = "E-mail invalide. Recommencez.";
|
||||
}
|
||||
}
|
||||
|
||||
const panelNav = document.querySelector(".panel");
|
||||
const navOverlay = document.querySelector("#nav-overlay");
|
||||
const openNavBtn = document.querySelector("button.open-nav");
|
||||
const openNavBtns = document.querySelectorAll("button.open-nav");
|
||||
const closeNavBtn = document.querySelector(".panel-close");
|
||||
function closeNav() {
|
||||
panelNav.classList.remove("panel--visible");
|
||||
|
|
@ -116,14 +136,18 @@ function closeNav() {
|
|||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
ragadjust("h1, h2, h3, h4, h5", ["all"]);
|
||||
ragadjust("h1, h2, h4, h5", ["all"]);
|
||||
window.window.scrollTo({
|
||||
top: 0,
|
||||
});
|
||||
|
||||
window.addEventListener("scroll", () => {
|
||||
const handleScroll = throttle(() => {
|
||||
toggleLogoState();
|
||||
});
|
||||
if (window.innerWidth <= 680) {
|
||||
toggleFooterState();
|
||||
}
|
||||
}, 100);
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
|
||||
setWindowHeightFactor();
|
||||
window.addEventListener("resize", () => {
|
||||
|
|
@ -169,10 +193,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
});
|
||||
});
|
||||
|
||||
openNavBtn.addEventListener("click", () => {
|
||||
panelNav.classList.add("panel--visible");
|
||||
navOverlay.classList.add("nav-overlay--visible");
|
||||
document.body.classList.add("no-scroll");
|
||||
openNavBtns.forEach((openNavBtn) => {
|
||||
openNavBtn.addEventListener("click", () => {
|
||||
panelNav.classList.add("panel--visible");
|
||||
navOverlay.classList.add("nav-overlay--visible");
|
||||
document.body.classList.add("no-scroll");
|
||||
});
|
||||
});
|
||||
|
||||
closeNavBtn.addEventListener("click", () => {
|
||||
|
|
@ -181,6 +207,4 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||
navOverlay.addEventListener("click", () => {
|
||||
closeNav();
|
||||
});
|
||||
|
||||
subscribeBtn.addEventListener("click", showSubscribeField);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,11 +23,21 @@
|
|||
},
|
||||
"require": {
|
||||
"php": "~8.1.0 || ~8.2.0 || ~8.3.0",
|
||||
"getkirby/cms": "^4.0"
|
||||
"getkirby/cms": "^4.5",
|
||||
"symfony/http-client": "^7.2",
|
||||
"nyholm/psr7": "^1.8",
|
||||
"php-http/guzzle7-adapter": "^1.1",
|
||||
"mailersend/mailersend": "^0.28.0",
|
||||
"sylvainjule/code-editor": "^1.0",
|
||||
"tobimori/kirby-seo": "^1.1"
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "8.3.0"
|
||||
},
|
||||
"allow-plugins": {
|
||||
"getkirby/composer-installer": true
|
||||
"getkirby/composer-installer": true,
|
||||
"php-http/discovery": true
|
||||
},
|
||||
"optimize-autoloader": true
|
||||
},
|
||||
|
|
|
|||
2333
composer.lock
generated
2333
composer.lock
generated
File diff suppressed because it is too large
Load diff
BIN
favicon-96x96.png
Normal file
BIN
favicon-96x96.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
BIN
favicon.ico
Normal file
BIN
favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
17
favicon.svg
Normal file
17
favicon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 38 KiB |
21
site.webmanifest
Normal file
21
site.webmanifest
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "actuel-inactuel",
|
||||
"short_name": "a-i",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#000000",
|
||||
"display": "standalone"
|
||||
}
|
||||
8
site/blueprints/fields/body.yml
Normal file
8
site/blueprints/fields/body.yml
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
label: Corps
|
||||
type: writer
|
||||
headings:
|
||||
- 3
|
||||
marks:
|
||||
- bold
|
||||
- italic
|
||||
- link
|
||||
|
|
@ -9,11 +9,16 @@ tabs:
|
|||
type: fields
|
||||
fields:
|
||||
presentation:
|
||||
extends: fields/body
|
||||
label: Présentation
|
||||
type: writer
|
||||
help: Optionnelle
|
||||
help: |
|
||||
Optionnelle, sans mention du poste. Peut inclure un lien.
|
||||
Exemple : "Co-fondateur des éditions [Athom](http://www.athom.xyz/)."
|
||||
texts:
|
||||
label: Textes
|
||||
type: pages
|
||||
create: false
|
||||
query: page.getTexts()
|
||||
seo:
|
||||
extends: seo/page
|
||||
label: Indexation
|
||||
|
|
|
|||
|
|
@ -7,3 +7,6 @@ tabs:
|
|||
label: Liste
|
||||
type: pages
|
||||
template: author
|
||||
seo:
|
||||
extends: seo/page
|
||||
label: Indexation
|
||||
|
|
|
|||
|
|
@ -6,21 +6,41 @@ image:
|
|||
back: black
|
||||
color: white
|
||||
icon: email
|
||||
options:
|
||||
changeStatus: false
|
||||
|
||||
tabs:
|
||||
contentTab:
|
||||
fields:
|
||||
month:
|
||||
label: Mois
|
||||
type: date
|
||||
display: "mmmm YYYY"
|
||||
default: today
|
||||
width: 1/4
|
||||
sendBtn:
|
||||
type: send-button
|
||||
width: 1/4
|
||||
content:
|
||||
label: Contenu
|
||||
type: writer
|
||||
label: Contenu
|
||||
icon: text
|
||||
columns:
|
||||
- width: 1/1
|
||||
fields:
|
||||
body:
|
||||
label: Contenu
|
||||
type: writer
|
||||
sendTab:
|
||||
label: Envoi
|
||||
icon: plane
|
||||
columns:
|
||||
- width: 1/4
|
||||
fields:
|
||||
published:
|
||||
label: Date d'envoi
|
||||
type: date
|
||||
help: Automatiquement rempli au moment de l'envoi.
|
||||
disabled: true
|
||||
width: 1/2
|
||||
- width: 1/2
|
||||
fields:
|
||||
testAdressList:
|
||||
label: Liste d'adresses de test
|
||||
type: structure
|
||||
sortBy: email asc
|
||||
help: Le bouton "Tester" ci-dessous tentera d'envoyer l'email aux adresses de cette liste
|
||||
fields:
|
||||
email:
|
||||
type: email
|
||||
sendBtn:
|
||||
type: send-button
|
||||
seo:
|
||||
extends: seo/page
|
||||
label: Indexation
|
||||
|
|
|
|||
|
|
@ -18,6 +18,17 @@ tabs:
|
|||
fullWidth:
|
||||
label: Pleine largeur
|
||||
type: toggle
|
||||
subtitle:
|
||||
label: Sous-titre
|
||||
type: writer
|
||||
help: optionnel
|
||||
marks:
|
||||
- italic
|
||||
node: false
|
||||
chapo:
|
||||
label: Chapo
|
||||
extends: fields/body
|
||||
help: optionnel
|
||||
body:
|
||||
label: Corps
|
||||
type: layout
|
||||
|
|
@ -31,4 +42,9 @@ tabs:
|
|||
- image
|
||||
- line
|
||||
- quote
|
||||
|
||||
metaTab: tabs/meta
|
||||
|
||||
seo:
|
||||
extends: seo/page
|
||||
label: Indexation
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ tabs:
|
|||
label: Corps
|
||||
type: fields
|
||||
fields:
|
||||
body:
|
||||
label: Corps
|
||||
type: writer
|
||||
body: fields/body
|
||||
seo:
|
||||
extends: seo/page
|
||||
label: Indexation
|
||||
|
|
|
|||
|
|
@ -15,7 +15,36 @@ tabs:
|
|||
label: Corps
|
||||
type: fields
|
||||
fields:
|
||||
body:
|
||||
label: Corps
|
||||
subtitle:
|
||||
label: Sous-titre
|
||||
type: writer
|
||||
help: optionnel
|
||||
marks:
|
||||
- italic
|
||||
node: false
|
||||
chapo:
|
||||
label: Chapo
|
||||
extends: fields/body
|
||||
help: optionnel
|
||||
body:
|
||||
extends: fields/body
|
||||
when:
|
||||
isHtmlMode: false
|
||||
htmlBody:
|
||||
label: Corps
|
||||
type: textarea
|
||||
buttons:
|
||||
- headlines
|
||||
- bold
|
||||
- italic
|
||||
- link
|
||||
when:
|
||||
isHtmlMode: true
|
||||
|
||||
paramsTab: tabs/params
|
||||
|
||||
metaTab: tabs/meta
|
||||
|
||||
seo:
|
||||
extends: seo/page
|
||||
label: Indexation
|
||||
|
|
|
|||
|
|
@ -3,19 +3,26 @@ title: Liste de diffusion
|
|||
tabs:
|
||||
content:
|
||||
columns:
|
||||
- width: 1/1
|
||||
fields:
|
||||
body:
|
||||
extends: fields/body
|
||||
- width: 1/2
|
||||
fields:
|
||||
subscribers:
|
||||
label: Abonnés
|
||||
type: structure
|
||||
sortBy: email asc
|
||||
fields:
|
||||
email:
|
||||
type: email
|
||||
- width: 1/2
|
||||
sections:
|
||||
newsletters:
|
||||
label: Emails
|
||||
label: Lettres
|
||||
type: pages
|
||||
template: email
|
||||
info: "{{ page.month.toDate('M Y') }}"
|
||||
sortBy: month desc
|
||||
info: "{{ page.status == 'listed' ? 'envoyée' : 'brouillon' }}"
|
||||
seo:
|
||||
extends: seo/page
|
||||
label: Indexation
|
||||
|
|
|
|||
|
|
@ -1,24 +1,30 @@
|
|||
title: texts
|
||||
|
||||
columns:
|
||||
- width: 1/2
|
||||
fields:
|
||||
categories:
|
||||
type: tags
|
||||
- width: 1/2
|
||||
sections:
|
||||
yearsSection:
|
||||
label: Années
|
||||
type: pages
|
||||
template: year
|
||||
sortBy: title desc
|
||||
- width: 1/1
|
||||
sections:
|
||||
allTextsSection:
|
||||
label: Tous les textes
|
||||
type: pages
|
||||
create: false
|
||||
search: true
|
||||
query: page.children.children
|
||||
info: "{{ page.author.toPage.title }} [{{ page.category }}]"
|
||||
sortBy: modified desc
|
||||
tabs:
|
||||
contentTab:
|
||||
label: Contenu
|
||||
columns:
|
||||
- width: 1/2
|
||||
fields:
|
||||
categories:
|
||||
type: tags
|
||||
- width: 1/2
|
||||
sections:
|
||||
yearsSection:
|
||||
label: Années
|
||||
type: pages
|
||||
template: year
|
||||
sortBy: title desc
|
||||
- width: 1/1
|
||||
sections:
|
||||
allTextsSection:
|
||||
label: Tous les textes
|
||||
type: pages
|
||||
create: false
|
||||
search: true
|
||||
query: page.children.children
|
||||
info: "{{ page.author.toPage.title }} [{{ page.category }}]"
|
||||
sortBy: modified desc
|
||||
seo:
|
||||
extends: seo/page
|
||||
label: Indexation
|
||||
|
|
|
|||
|
|
@ -4,16 +4,30 @@ image:
|
|||
back: black
|
||||
color: white
|
||||
|
||||
columns:
|
||||
- width: 1/3
|
||||
sections:
|
||||
texts:
|
||||
label: Textes
|
||||
type: pages
|
||||
templates:
|
||||
- linear
|
||||
- grid
|
||||
- width: 2/3
|
||||
fields:
|
||||
edito:
|
||||
type: writer
|
||||
tabs:
|
||||
contentTab:
|
||||
label: Contenu
|
||||
columns:
|
||||
- width: 1/3
|
||||
sections:
|
||||
fieldsSection:
|
||||
type: fields
|
||||
fields:
|
||||
openDate:
|
||||
label: Date d'ouverture
|
||||
type: date
|
||||
display: DD/MM/YYYY
|
||||
texts:
|
||||
label: Textes
|
||||
type: pages
|
||||
help: **Pour réorganiser les textes**, utiliser la poignée ⁝⁝ qui apparait au survol.
|
||||
templates:
|
||||
- linear
|
||||
- grid
|
||||
- width: 2/3
|
||||
fields:
|
||||
edito:
|
||||
type: writer
|
||||
seo:
|
||||
extends: seo/page
|
||||
label: Indexation
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ tabs:
|
|||
type: writer
|
||||
nodes: false
|
||||
marks: false
|
||||
edito:
|
||||
label: Éditorial
|
||||
type: writer
|
||||
edito: fields/body
|
||||
seo:
|
||||
extends: seo/site
|
||||
label: Indexation
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ sections:
|
|||
metadata:
|
||||
type: fields
|
||||
fields:
|
||||
keywords:
|
||||
label: Mots-clés
|
||||
type: tags
|
||||
published:
|
||||
label: Date de publication
|
||||
type: date
|
||||
|
|
|
|||
13
site/blueprints/tabs/params.yml
Normal file
13
site/blueprints/tabs/params.yml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
label: Paramètres
|
||||
fields:
|
||||
isHtmlMode:
|
||||
label: Mode HTML
|
||||
type: toggle
|
||||
width: 1/3
|
||||
help: |
|
||||
Actif : le code HTML du corps sera exécuté.
|
||||
additionnalCss:
|
||||
label: CSS additionnel
|
||||
type: code-editor
|
||||
help: Le point de rupture mobile / ordinateur se situe à 640px.
|
||||
width: 2/3
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
title: Auteur
|
||||
description: Droits de lecture et écriture de certaines pages.
|
||||
image:
|
||||
icon: pen
|
||||
color: "#FFF"
|
||||
|
||||
permissions:
|
||||
access:
|
||||
*: false
|
||||
files:
|
||||
*: false
|
||||
languages:
|
||||
*: false
|
||||
pages:
|
||||
*: false
|
||||
site:
|
||||
*: false
|
||||
user:
|
||||
*: false
|
||||
users:
|
||||
*: false
|
||||
|
||||
fields:
|
||||
presentation:
|
||||
type: writer
|
||||
|
|
@ -1,16 +1,17 @@
|
|||
<?php
|
||||
|
||||
function createEmptyCategories() {
|
||||
function createEmptyCategories()
|
||||
{
|
||||
$categories = new Pages();
|
||||
foreach (page('textes')->categories()->split() as $categoryName) {
|
||||
$category = new Page([
|
||||
'slug' => Str::slug($categoryName),
|
||||
'slug' => Str::slug($categoryName),
|
||||
'template' => 'category',
|
||||
'status' => 'listed',
|
||||
'content' => [
|
||||
'status' => 'listed',
|
||||
'content' => [
|
||||
'title' => $categoryName,
|
||||
],
|
||||
'children' => []
|
||||
'children' => [],
|
||||
]);
|
||||
|
||||
$categories->add($category);
|
||||
|
|
@ -20,7 +21,8 @@ function createEmptyCategories() {
|
|||
}
|
||||
|
||||
|
||||
function createCategories() {
|
||||
function createCategories()
|
||||
{
|
||||
$emptyCategories = createEmptyCategories();
|
||||
foreach (page('textes')->grandChildren() as $text) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,25 +1,29 @@
|
|||
<?php
|
||||
|
||||
return array(
|
||||
return [
|
||||
'debug' => true,
|
||||
'panel' => array(
|
||||
'panel' => [
|
||||
'menu' => require __DIR__ . '/menu.php',
|
||||
'css' => 'assets/css/panel.css'
|
||||
),
|
||||
'email' => [
|
||||
'transport' => [
|
||||
'type' => 'smtp',
|
||||
'host' => 'smtp.outlook.com',
|
||||
'port' => 587,
|
||||
'security' => 'tls',
|
||||
'auth' => true,
|
||||
'username' => 'adrien.payet@outlook.com',
|
||||
'password' => 't8nVpxCpEZcqH8y'
|
||||
]
|
||||
'css' => 'assets/css/panel.css',
|
||||
],
|
||||
'routes' => array(
|
||||
require __DIR__ . '/routes/virtual-author.php',
|
||||
'mailerSendApiKey' => 'mlsn.0a9f20751951e3c2d130b1d6c3749b0a0f5b14f1c52da65a3369d658c736513c',
|
||||
'email' => [
|
||||
'transport' => [
|
||||
'type' => 'smtp',
|
||||
'host' => 'smtp.mailersend.net',
|
||||
'port' => 587,
|
||||
'security' => true,
|
||||
'auth' => true,
|
||||
'username' => 'MS_ncQ2K5@actuel-inactuel.fr',
|
||||
'password' => 'mssp.ou3hOyX.z86org8y2kklew13.raOTfvP',
|
||||
],
|
||||
],
|
||||
'routes' => [
|
||||
require __DIR__ . '/routes/virtual-category.php',
|
||||
require __DIR__ . '/routes/send-newsletter.php',
|
||||
),
|
||||
);
|
||||
require __DIR__ . '/routes/subscribe.php',
|
||||
require __DIR__ . '/routes/virtual-pending.php',
|
||||
],
|
||||
'hooks' => [
|
||||
'page.create:after' => require __DIR__ . '/hooks/prefill-test-adress-list.php',
|
||||
],
|
||||
];
|
||||
|
|
|
|||
17
site/config/hooks/prefill-test-adress-list.php
Normal file
17
site/config/hooks/prefill-test-adress-list.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
return function ($page) {
|
||||
if ($page->template() == 'email') {
|
||||
$list = [
|
||||
['email' => 'fournelcecile@yahoo.fr'],
|
||||
['email' => 'payet.adrien@protonmail.com'],
|
||||
['email' => 'mazet.zaccardelli@free.fr'],
|
||||
['email' => 'wafaabida@hotmail.com'],
|
||||
['email' => 'elisegarraud@yahoo.fr'],
|
||||
['email' => 'pierre-damien.huyghe@univ-paris1.fr'],
|
||||
];
|
||||
$page->update([
|
||||
'testAdressList' => Yaml::encode($list),
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
|
@ -2,62 +2,62 @@
|
|||
|
||||
return [
|
||||
'site' => [
|
||||
'label' => 'Accueil',
|
||||
'label' => 'Accueil',
|
||||
'current' => function ($current) {
|
||||
$path = Kirby::instance()->request()->path()->toString();
|
||||
return Str::contains($path, 'site');
|
||||
}
|
||||
},
|
||||
],
|
||||
'texts' => [
|
||||
'icon' => 'pen',
|
||||
'label' => 'Textes',
|
||||
'link' => 'pages/textes',
|
||||
'icon' => 'pen',
|
||||
'label' => 'Textes',
|
||||
'link' => 'pages/textes',
|
||||
'current' => function ($current) {
|
||||
$path = Kirby::instance()->request()->path()->toString();
|
||||
return Str::contains($path, 'pages/textes');
|
||||
}
|
||||
},
|
||||
],
|
||||
'authors' => [
|
||||
'icon' => 'users',
|
||||
'label' => 'Auteurs',
|
||||
'link' => 'pages/auteurs',
|
||||
'icon' => 'users',
|
||||
'label' => 'Auteurs',
|
||||
'link' => 'pages/auteurs',
|
||||
'current' => function ($current) {
|
||||
$path = Kirby::instance()->request()->path()->toString();
|
||||
return Str::contains($path, 'pages/auteurs');
|
||||
}
|
||||
$path = Kirby::instance()->request()->path()->toString();
|
||||
return Str::contains($path, 'pages/auteurs');
|
||||
},
|
||||
],
|
||||
'-',
|
||||
'-',
|
||||
'infos' => [
|
||||
'icon' => 'question',
|
||||
'label' => 'À propos',
|
||||
'link' => 'pages/a-propos',
|
||||
'icon' => 'question',
|
||||
'label' => 'À propos',
|
||||
'link' => 'pages/a-propos',
|
||||
'current' => function ($current) {
|
||||
$path = Kirby::instance()->request()->path()->toString();
|
||||
return Str::contains($path, 'pages/a-propos');
|
||||
}
|
||||
},
|
||||
],
|
||||
'newsletter' => [
|
||||
'icon' => 'email',
|
||||
'label' => 'Liste de diffusion',
|
||||
'link' => 'pages/liste-de-diffusion',
|
||||
'subscription' => [
|
||||
'icon' => 'email',
|
||||
'label' => 'Liste de diffusion',
|
||||
'link' => 'pages/lettre',
|
||||
'current' => function ($current) {
|
||||
$path = Kirby::instance()->request()->path()->toString();
|
||||
return Str::contains($path, 'pages/liste-de-diffusion');
|
||||
}
|
||||
},
|
||||
],
|
||||
'-',
|
||||
'-',
|
||||
'users',
|
||||
'comments',
|
||||
'admin' => [
|
||||
'icon' => 'folder',
|
||||
'label' => 'Administration',
|
||||
'link' => 'pages/admin',
|
||||
'icon' => 'folder',
|
||||
'label' => 'Administration',
|
||||
'link' => 'pages/admin',
|
||||
'current' => function ($current) {
|
||||
$path = Kirby::instance()->request()->path()->toString();
|
||||
return Str::contains($path, 'pages/admin');
|
||||
}
|
||||
},
|
||||
],
|
||||
'-',
|
||||
'-',
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'pattern' => '/send-newsletter.json',
|
||||
'method' => 'POST',
|
||||
'action' => function () {
|
||||
$jsonRequest = file_get_contents("php://input");
|
||||
$request = json_decode($jsonRequest);
|
||||
|
||||
$kirby = kirby();
|
||||
|
||||
try {
|
||||
$kirby->email([
|
||||
'from' => "adrien.payet@outlook.com",
|
||||
'to' => 'payet.adrien@protonmail.com',
|
||||
'subject' => 'actualités',
|
||||
'body' => 'Ceci est un test simple.',
|
||||
]);
|
||||
return json_encode(['status' => 'success', 'message' => 'Email envoyé avec succès.']);
|
||||
} catch (Exception $error) {
|
||||
return json_encode(['status' => 'error', 'message' => $error->getMessage()]);
|
||||
}
|
||||
}
|
||||
];
|
||||
43
site/config/routes/subscribe.php
Normal file
43
site/config/routes/subscribe.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'pattern' => '/subscribe.json',
|
||||
'method' => 'POST',
|
||||
'action' => function () {
|
||||
$jsonRequest = file_get_contents('php://input');
|
||||
$email = Str::lower(json_decode($jsonRequest));
|
||||
|
||||
if (V::email($email)) {
|
||||
kirby()->impersonate('kirby');
|
||||
|
||||
$page = page('lettre');
|
||||
$subscribers = $page->subscribers()->yaml();
|
||||
|
||||
$emailExists = in_array(['email' => $email], $subscribers);
|
||||
|
||||
if ($emailExists) {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'Cet email est déjà inscris.',
|
||||
];
|
||||
}
|
||||
|
||||
$newSubscriber = ['email' => $email];
|
||||
$subscribers[] = $newSubscriber;
|
||||
|
||||
$page->update([
|
||||
'subscribers' => $subscribers,
|
||||
]);
|
||||
|
||||
return [
|
||||
'status' => 'success',
|
||||
'message' => 'lettre réussie.',
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
'status' => 'error',
|
||||
'message' => 'Email invalide.',
|
||||
];
|
||||
}
|
||||
},
|
||||
];
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Uuid\Uuid;
|
||||
|
||||
return [
|
||||
'pattern' => 'auteurs/(:any)',
|
||||
'action' => function ($slug) {
|
||||
$kirby = kirby();
|
||||
$author = getAuthorBySlug($slug);
|
||||
|
||||
return Page::factory(
|
||||
[
|
||||
'slug' => '',
|
||||
'template' => 'author',
|
||||
'model' => 'authors',
|
||||
'content' => [
|
||||
'title' => $author->name(),
|
||||
'presentation' => $author->presentation(),
|
||||
'author' => $author->name(),
|
||||
'uuid' => Uuid::generate(),
|
||||
]
|
||||
]
|
||||
);
|
||||
}
|
||||
];
|
||||
|
|
@ -5,18 +5,33 @@ use Kirby\Uuid\Uuid;
|
|||
return [
|
||||
'pattern' => 'categories/(:any)',
|
||||
'action' => function ($category) {
|
||||
$kirby = kirby();
|
||||
|
||||
return Page::factory(
|
||||
[
|
||||
'slug' => '',
|
||||
'template' => 'category',
|
||||
'model' => 'categories',
|
||||
'content' => [
|
||||
'title' => $category,
|
||||
'uuid' => Uuid::generate(),
|
||||
]
|
||||
]
|
||||
$allTexts = page('textes')->grandChildren();
|
||||
$textsInCategory = $allTexts->filter(
|
||||
fn($text) => Str::slug($text->category()) === $category
|
||||
);
|
||||
}
|
||||
|
||||
$texts = [];
|
||||
|
||||
foreach ($textsInCategory as $text) {
|
||||
$texts[] = (string) $text->uri();
|
||||
}
|
||||
|
||||
try {
|
||||
$title = $textsInCategory->first()->category();
|
||||
} catch (\Throwable $th) {
|
||||
go('/erreur');
|
||||
}
|
||||
|
||||
return Page::factory([
|
||||
'slug' => Str::slug($category) . '-' . Uuid::generate(),
|
||||
'template' => 'category',
|
||||
'model' => 'category',
|
||||
'content' => [
|
||||
'title' => $title,
|
||||
'texts' => Yaml::encode($texts),
|
||||
'uuid' => Uuid::generate(),
|
||||
],
|
||||
]);
|
||||
},
|
||||
];
|
||||
|
|
|
|||
18
site/config/routes/virtual-pending.php
Normal file
18
site/config/routes/virtual-pending.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Uuid\Uuid;
|
||||
|
||||
return [
|
||||
'pattern' => 'a-venir',
|
||||
'action' => function () {
|
||||
return Page::factory([
|
||||
'slug' => 'a-venir',
|
||||
'template' => 'error',
|
||||
'content' => [
|
||||
'title' => 'Textes disponibles à partir du 21 janvier',
|
||||
'body' => '<p>Rendez-vous vendredi 21 février à partir de 18h, au Centre Césure (ancien campus Censier), 13 rue Santeuil, 75005 Paris - salle Las Vergnas (métro Censier-Daubenton ou Saint-Marcel).</p>',
|
||||
'uuid' => Uuid::generate(),
|
||||
],
|
||||
]);
|
||||
},
|
||||
];
|
||||
12
site/plugins/code-editor/.editorconfig
Normal file
12
site/plugins/code-editor/.editorconfig
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.php]
|
||||
indent_size = 4
|
||||
15
site/plugins/code-editor/.eslintrc.json
Normal file
15
site/plugins/code-editor/.eslintrc.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:vue/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
}
|
||||
}
|
||||
2
site/plugins/code-editor/.gitignore
vendored
Normal file
2
site/plugins/code-editor/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
21
site/plugins/code-editor/LICENSE
Normal file
21
site/plugins/code-editor/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 Sylvain Julé
|
||||
|
||||
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.
|
||||
111
site/plugins/code-editor/README.md
Normal file
111
site/plugins/code-editor/README.md
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# Kirby – Code editor
|
||||
|
||||
Code editor field for Kirby 3 and 4.
|
||||
|
||||

|
||||
|
||||
<br/>
|
||||
|
||||
## Overview
|
||||
|
||||
> This plugin is completely free and published under the MIT license. However, if you are using it in a commercial project and want to help me keep up with maintenance, please consider [making a donation of your choice](https://paypal.me/sylvainjl) or purchasing your license(s) through [my affiliate link](https://a.paddle.com/v2/click/1129/36369?link=1170).
|
||||
|
||||
- [1. Installation](#1-installation)
|
||||
- [2. Setup](#2-setup)
|
||||
- [3. Options](#3-options)
|
||||
- [4. Available languages](#4-available-languages)
|
||||
- [5. License](#5-license)
|
||||
- [6. Credits](#6-credits)
|
||||
|
||||
<br/>
|
||||
|
||||
## 1. Installation
|
||||
|
||||
Download and copy this repository to ```/site/plugins/code-editor```
|
||||
|
||||
Alternatively, you can install it with composer: ```composer require sylvainjule/code-editor```
|
||||
|
||||
<br/>
|
||||
|
||||
## 2. Setup
|
||||
|
||||
This field adds a code editor in the panel:
|
||||
|
||||
```yaml
|
||||
editor:
|
||||
label: My code editor
|
||||
type: code-editor
|
||||
```
|
||||
|
||||
<br/>
|
||||
|
||||
## 3. Options
|
||||
|
||||
| Name | Type | Default | Options | Description |
|
||||
| -------------------- | ------------------ | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| language | `String` | `'css'` | - | Syntax mode of the editor. See below for available languages |
|
||||
| size | `String` | `'small'` | - | Min height of the editor. `small / medium / large / huge` |
|
||||
| lineNumbers | `Boolean` | `true` | - | Whether to show line numbers. |
|
||||
| tabSize | `number` | `4` | - | The number of characters to insert when pressing tab key. |
|
||||
| insertSpaces | `boolean` | `true` | - | Whether to use spaces for indentation. If you set it to `false`, you might also want to set `tabSize` to `1` |
|
||||
| ignoreTabKey | `boolean` | `false` | - | Whether the editor should ignore tab key presses so that keyboard users can tab past the editor. Users can toggle this behaviour using `Ctrl+Shift+M` (Mac) / `Ctrl+M` manually when this is `false`. |
|
||||
|
||||
|
||||
Note that you can make the default height any height you want with some [custom panel CSS](https://getkirby.com/docs/reference/system/options/panel#custom-panel-css). First, set the `size` option to any string you'd like:
|
||||
|
||||
```yaml
|
||||
size: custom-size
|
||||
```
|
||||
|
||||
Then in your `panel.css`:
|
||||
|
||||
```css
|
||||
.k-code-editor-input[data-size="custom-size"] {
|
||||
min-height: 15rem;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.1. Default options
|
||||
|
||||
You can globally override the default options, instead of setting them on a per-field basis. In your `site/config/config.php`:
|
||||
|
||||
```php
|
||||
return [
|
||||
'sylvainjule.code-editor.language' => 'css',
|
||||
'sylvainjule.code-editor.size' => 'small',
|
||||
'sylvainjule.code-editor.lineNumbers' => true,
|
||||
'sylvainjule.code-editor.tabSize' => 4,
|
||||
'sylvainjule.code-editor.insertSpaces' => true,
|
||||
'sylvainjule.code-editor.ignoreTabKey' => false,
|
||||
];
|
||||
```
|
||||
|
||||
<br/>
|
||||
|
||||
## 4. Available languages
|
||||
|
||||
Currently supported languages are:
|
||||
|
||||
* `css`
|
||||
* `javascript`
|
||||
* `json`
|
||||
* `less`
|
||||
* `php`
|
||||
* `python`
|
||||
* `ruby`
|
||||
* `scss`
|
||||
* `yaml`
|
||||
|
||||
<br/>
|
||||
|
||||
## 5. License
|
||||
|
||||
MIT
|
||||
|
||||
<br/>
|
||||
|
||||
## 6. Credits
|
||||
|
||||
**Code editor:**
|
||||
|
||||
- [Vue Prism Editor](https://github.com/koca/vue-prism-editor)
|
||||
20
site/plugins/code-editor/composer.json
Normal file
20
site/plugins/code-editor/composer.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "sylvainjule/code-editor",
|
||||
"description": "Code editor field for Kirby 3 and 4",
|
||||
"type": "kirby-plugin",
|
||||
"license": "MIT",
|
||||
"version": "1.0.3",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Sylvain Julé",
|
||||
"email": "contact@sylvain-jule.fr"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"getkirby/composer-installer": "^1.1"
|
||||
},
|
||||
"extra": {
|
||||
"installer-name": "code-editor"
|
||||
},
|
||||
"minimum-stability": "beta"
|
||||
}
|
||||
1
site/plugins/code-editor/index.css
Normal file
1
site/plugins/code-editor/index.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
.prism-editor-wrapper{width:100%;height:100%;display:flex;align-items:flex-start;overflow:auto;-o-tab-size:1.5em;tab-size:1.5em;-moz-tab-size:1.5em}@media (-ms-high-contrast:active),(-ms-high-contrast:none){.prism-editor-wrapper .prism-editor__textarea{color:transparent!important}.prism-editor-wrapper .prism-editor__textarea::-moz-selection{background-color:#accef7!important;color:transparent!important}.prism-editor-wrapper .prism-editor__textarea::selection{background-color:#accef7!important;color:transparent!important}}.prism-editor-wrapper .prism-editor__container{position:relative;text-align:left;box-sizing:border-box;padding:0;overflow:hidden;width:100%}.prism-editor-wrapper .prism-editor__line-numbers{height:100%;overflow:hidden;flex-shrink:0;padding-top:4px;margin-top:0;margin-right:10px}.prism-editor-wrapper .prism-editor__line-number{text-align:right;white-space:nowrap}.prism-editor-wrapper .prism-editor__textarea{position:absolute;top:0;left:0;height:100%;width:100%;resize:none;color:inherit;overflow:hidden;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;-webkit-text-fill-color:transparent}.prism-editor-wrapper .prism-editor__editor,.prism-editor-wrapper .prism-editor__textarea{margin:0;border:0;background:none;box-sizing:inherit;display:inherit;font-family:inherit;font-size:inherit;font-style:inherit;font-variant-ligatures:inherit;font-weight:inherit;letter-spacing:inherit;line-height:inherit;-moz-tab-size:inherit;-o-tab-size:inherit;tab-size:inherit;text-indent:inherit;text-rendering:inherit;text-transform:inherit;white-space:pre-wrap;word-wrap:keep-all;overflow-wrap:break-word;padding:0}.prism-editor-wrapper .prism-editor__textarea--empty{-webkit-text-fill-color:inherit!important}.prism-editor-wrapper .prism-editor__editor{position:relative;pointer-events:none}code[class*=language-],pre[class*=language-]{color:#ccc;background:none;font-family:Consolas,Monaco,Andale Mono,Ubuntu Mono,monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.comment,.token.block-comment,.token.prolog,.token.doctype,.token.cdata{color:#999}.token.punctuation{color:#ccc}.token.tag,.token.attr-name,.token.namespace,.token.deleted{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.number,.token.function{color:#f08d49}.token.property,.token.class-name,.token.constant,.token.symbol{color:#f8c555}.token.selector,.token.important,.token.atrule,.token.keyword,.token.builtin{color:#cc99cd}.token.string,.token.char,.token.attr-value,.token.regex,.token.variable{color:#7ec699}.token.operator,.token.entity,.token.url{color:#67cdcc}.token.important,.token.bold{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}.k-code-editor-input{background:#2d2d2d;color:#ccc;font-family:Fira code,Fira Mono,Consolas,Menlo,Courier,monospace;font-size:.9rem;line-height:1.5;padding:10px}.k-code-editor-input[data-size=small]{min-height:7.5rem}.k-code-editor-input[data-size=medium]{min-height:15rem}.k-code-editor-input[data-size=large],.k-code-editor-input[data-size=huge]{min-height:30rem}.prism-editor__textarea:focus{outline:none}
|
||||
14
site/plugins/code-editor/index.js
Normal file
14
site/plugins/code-editor/index.js
Normal file
File diff suppressed because one or more lines are too long
15
site/plugins/code-editor/index.php
Normal file
15
site/plugins/code-editor/index.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
Kirby::plugin('sylvainjule/code-editor', [
|
||||
'options' => [
|
||||
'language' => 'css',
|
||||
'size' => 'small',
|
||||
'lineNumbers' => true,
|
||||
'tabSize' => 4,
|
||||
'insertSpaces' => true,
|
||||
'ignoreTabKey' => false,
|
||||
],
|
||||
'fields' => [
|
||||
'code-editor' => require_once __DIR__ . '/lib/fields/code-editor.php',
|
||||
],
|
||||
]);
|
||||
32
site/plugins/code-editor/lib/fields/code-editor.php
Normal file
32
site/plugins/code-editor/lib/fields/code-editor.php
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
$options = require kirby()->root('kirby') . '/config/fields/textarea.php';
|
||||
|
||||
/* Merge new properties
|
||||
--------------------------------*/
|
||||
|
||||
$options = A::merge($options, [
|
||||
'props' => [
|
||||
'size' => function($size = null) {
|
||||
return $size ?? option('sylvainjule.code-editor.size');
|
||||
},
|
||||
'language' => function($language = null) {
|
||||
return $language ?? option('sylvainjule.code-editor.language');
|
||||
},
|
||||
'lineNumbers' => function($lineNumbers = null) {
|
||||
return $lineNumbers ?? option('sylvainjule.code-editor.lineNumbers');
|
||||
},
|
||||
'tabSize' => function($tabSize = null) {
|
||||
return $tabSize ?? option('sylvainjule.code-editor.tabSize');
|
||||
},
|
||||
'insertSpaces' => function($insertSpaces = null) {
|
||||
return $tabSize ?? option('sylvainjule.code-editor.insertSpaces');
|
||||
},
|
||||
'ignoreTabKey' => function($ignoreTabKey = null) {
|
||||
return $tabSize ?? option('sylvainjule.code-editor.ignoreTabKey');
|
||||
},
|
||||
]
|
||||
]);
|
||||
|
||||
// return the updated options
|
||||
return $options;
|
||||
6615
site/plugins/code-editor/package-lock.json
generated
Normal file
6615
site/plugins/code-editor/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
30
site/plugins/code-editor/package.json
Normal file
30
site/plugins/code-editor/package.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "kirby-code-editor",
|
||||
"version": "1.0.3",
|
||||
"description": "Code editor field for Kirby 3 and 4",
|
||||
"main": "index.js",
|
||||
"author": "Kirby Community",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:sylvainjule/kirby-code-editor.git"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "kirbyup src/index.js --watch",
|
||||
"build": "kirbyup src/index.js",
|
||||
"lint": "eslint \"src/**/*.{js,vue}\"",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"format": "prettier --write \"src/**/*.{css,js,vue}\"",
|
||||
"prepare": "node src/node/patchVuePrismEditor.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"consola": "^2.15.3",
|
||||
"eslint": "^8.3.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-vue": "^8.1.1",
|
||||
"kirbyup": "^0.21.1",
|
||||
"prettier": "^2.5.0",
|
||||
"prismjs": "^1.25.0",
|
||||
"vue-prism-editor": "^1.3.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
/* Colors
|
||||
---------------------------------*/
|
||||
|
||||
$color-black: #000;
|
||||
$color-white: #fff;
|
||||
$color-dark: #16171a;
|
||||
$color-dark-grey: #777;
|
||||
$color-light: #efefef;
|
||||
$color-light-grey: #999;
|
||||
$color-background: $color-light;
|
||||
$color-positive: #5d800d;
|
||||
$color-positive-border: $color-positive;
|
||||
$color-positive-outline: rgba($color-positive, 0.25);
|
||||
$color-positive-on-dark: #a7bd68;
|
||||
$color-focus: #4271ae;
|
||||
$color-focus-border: $color-focus;
|
||||
$color-focus-outline: rgba($color-focus, 0.25);
|
||||
$color-focus-on-dark: #81a2be;
|
||||
$color-notice: #f5871f;
|
||||
$color-notice-on-dark: #de935f;
|
||||
$color-negative: #c82829;
|
||||
$color-negative-border: $color-negative;
|
||||
$color-negative-outline: rgba($color-negative, 0.25);
|
||||
$color-negative-on-dark: #d16464;
|
||||
$color-border: #ccc;
|
||||
$color-backdrop: rgba($color-dark, 0.6);
|
||||
$color-inset: #ebebeb;
|
||||
|
||||
|
||||
/* Breakpoint
|
||||
---------------------------------*/
|
||||
|
||||
$breakpoint-small: 30em;
|
||||
$breakpoint-menu: 45em;
|
||||
$breakpoint-medium: 65em;
|
||||
$breakpoint-large: 90em;
|
||||
$breakpoint-huge: 120em;
|
||||
|
||||
|
||||
/* Fields
|
||||
---------------------------------*/
|
||||
|
||||
$field-input-padding: .5rem;
|
||||
$field-input-height: 2.25rem;
|
||||
$field-input-line-height: 1.25rem;
|
||||
|
||||
|
||||
/* Typography
|
||||
---------------------------------*/
|
||||
|
||||
$font-size-tiny: 0.75rem;
|
||||
$font-size-small: 0.875rem;
|
||||
$font-size-medium: 1rem;
|
||||
$font-size-large: 1.25rem;
|
||||
$font-size-huge: 1.5rem;
|
||||
$font-size-monster: 1.75rem;
|
||||
|
||||
$font-weight-normal: 400;
|
||||
$font-weight-bold: 600;
|
||||
|
||||
$font-family-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
$font-family-mono: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
29
site/plugins/code-editor/src/assets/css/styles.scss
Normal file
29
site/plugins/code-editor/src/assets/css/styles.scss
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
@import
|
||||
'abstracts/variables.scss';
|
||||
|
||||
.k-code-editor-input {
|
||||
background: #2d2d2d;
|
||||
color: #ccc;
|
||||
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace;
|
||||
font-size: .9rem;
|
||||
line-height: 1.5;
|
||||
padding: 10px;
|
||||
|
||||
&[data-size="small"] {
|
||||
min-height: 7.5rem;
|
||||
}
|
||||
&[data-size="medium"] {
|
||||
min-height: 15rem;
|
||||
}
|
||||
&[data-size="large"] {
|
||||
min-height: 30rem;
|
||||
}
|
||||
&[data-size="huge"] {
|
||||
min-height: 30rem;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.prism-editor__textarea:focus {
|
||||
outline: none;
|
||||
}
|
||||
72
site/plugins/code-editor/src/components/field/CodeEditor.vue
Normal file
72
site/plugins/code-editor/src/components/field/CodeEditor.vue
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<template>
|
||||
<k-field :input="uid" v-bind="$props" class="k-code-editor-field">
|
||||
<prism-editor
|
||||
v-model="code"
|
||||
class="k-code-editor-input"
|
||||
:highlight="highlighter"
|
||||
:line-numbers="lineNumbers"
|
||||
:tab-size="tabSize"
|
||||
:insert-spaces="insertSpaces"
|
||||
:ignore-tab-key="ignoreTabKey"
|
||||
:data-size="size"
|
||||
@input="onCodeInput"
|
||||
/>
|
||||
</k-field>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { PrismEditor } from "vue-prism-editor";
|
||||
import "vue-prism-editor/dist/prismeditor.min.css";
|
||||
import { highlight, languages } from "prismjs/components/prism-core";
|
||||
import "prismjs/components/prism-markup-templating";
|
||||
import "prismjs/components/prism-clike";
|
||||
import "prismjs/components/prism-css";
|
||||
import "prismjs/components/prism-javascript";
|
||||
import "prismjs/components/prism-json";
|
||||
import "prismjs/components/prism-less";
|
||||
import "prismjs/components/prism-php";
|
||||
import "prismjs/components/prism-python";
|
||||
import "prismjs/components/prism-ruby";
|
||||
import "prismjs/components/prism-scss";
|
||||
import "prismjs/components/prism-yaml";
|
||||
import "prismjs/themes/prism-tomorrow.css";
|
||||
|
||||
export default {
|
||||
components: { PrismEditor },
|
||||
|
||||
extends: "k-textarea-field",
|
||||
|
||||
props: {
|
||||
size: String,
|
||||
language: String,
|
||||
lineNumbers: Boolean,
|
||||
tabSize: Number,
|
||||
insertSpaces: Boolean,
|
||||
ignoreTabKey: Boolean,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
code: "",
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.code = this.value;
|
||||
},
|
||||
|
||||
methods: {
|
||||
highlighter() {
|
||||
return highlight(this.code, languages[this.language]);
|
||||
},
|
||||
|
||||
onCodeInput() {
|
||||
this.$emit("input", this.code);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "../../assets/css/styles.scss";
|
||||
</style>
|
||||
7
site/plugins/code-editor/src/index.js
Normal file
7
site/plugins/code-editor/src/index.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import CodeEditor from "./components/field/CodeEditor.vue";
|
||||
|
||||
window.panel.plugin("sylvainjule/code-editor", {
|
||||
fields: {
|
||||
"code-editor": CodeEditor,
|
||||
},
|
||||
});
|
||||
38
site/plugins/code-editor/src/node/patchVuePrismEditor.mjs
Normal file
38
site/plugins/code-editor/src/node/patchVuePrismEditor.mjs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { existsSync, readFileSync, writeFileSync } from "fs";
|
||||
import chalk from "chalk";
|
||||
import consola from "consola";
|
||||
|
||||
const srcPath = "node_modules/vue-prism-editor/dist/prismeditor.esm.js";
|
||||
|
||||
async function main() {
|
||||
consola.start("Vue Prism Editor patcher");
|
||||
|
||||
if (!existsSync(srcPath)) {
|
||||
consola.error(
|
||||
`couldn't find ${chalk.cyan(srcPath)}, did you run ${chalk.green(
|
||||
"npm install"
|
||||
)}?`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const source = readFileSync(srcPath, "utf8");
|
||||
|
||||
if (!source.includes("Vue.extend")) {
|
||||
consola.success("already patched");
|
||||
return;
|
||||
}
|
||||
|
||||
consola.info("patching the source component...");
|
||||
|
||||
let output = source
|
||||
.replace(/^import Vue from 'vue';/, "")
|
||||
.replace("/*#__PURE__*/Vue.extend(", "")
|
||||
.replace(/\}\)(;\s+export)/, "}$1");
|
||||
|
||||
writeFileSync(srcPath, output, "utf8");
|
||||
|
||||
consola.success("successfully patched");
|
||||
}
|
||||
|
||||
main().catch((err) => consola.error(err));
|
||||
|
|
@ -23,15 +23,10 @@ function setTitleFontSizeClass($title, $level = 'h1')
|
|||
|
||||
function getAuthorBySlug($slug)
|
||||
{
|
||||
$kirby = kirby();
|
||||
$author = '';
|
||||
foreach ($kirby->users() as $user) {
|
||||
|
||||
if (Str::slug($user->name()) === $slug) {
|
||||
|
||||
$author = $user;
|
||||
}
|
||||
}
|
||||
$site = site();
|
||||
$authors = page("auteurs")->children();
|
||||
$author = $authors->find($slug);
|
||||
|
||||
return $author;
|
||||
}
|
||||
|
||||
4
site/plugins/kirby-seo/.husky/pre-commit
Executable file
4
site/plugins/kirby-seo/.husky/pre-commit
Executable file
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
pnpm run build && git add index.css index.js
|
||||
1
site/plugins/kirby-seo/.nvmrc
Normal file
1
site/plugins/kirby-seo/.nvmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
20
|
||||
59
site/plugins/kirby-seo/.php-cs-fixer.dist.php
Normal file
59
site/plugins/kirby-seo/.php-cs-fixer.dist.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
$finder = PhpCsFixer\Finder::create()
|
||||
->exclude('vendor')
|
||||
->in(__DIR__);
|
||||
|
||||
$config = new PhpCsFixer\Config();
|
||||
return $config
|
||||
->setRules([
|
||||
'@PSR12' => true,
|
||||
'align_multiline_comment' => ['comment_type' => 'phpdocs_like'],
|
||||
'array_indentation' => true,
|
||||
'array_syntax' => ['syntax' => 'short'],
|
||||
'cast_spaces' => ['space' => 'none'],
|
||||
// 'class_keyword_remove' => true, // replaces static::class with 'static' (won't work)
|
||||
'combine_consecutive_issets' => true,
|
||||
'combine_consecutive_unsets' => true,
|
||||
'combine_nested_dirname' => true,
|
||||
'concat_space' => ['spacing' => 'one'],
|
||||
'declare_equal_normalize' => ['space' => 'single'],
|
||||
'dir_constant' => true,
|
||||
'function_typehint_space' => true,
|
||||
'include' => true,
|
||||
'logical_operators' => true,
|
||||
'lowercase_cast' => true,
|
||||
'lowercase_static_reference' => true,
|
||||
'magic_constant_casing' => true,
|
||||
'magic_method_casing' => true,
|
||||
'method_chaining_indentation' => true,
|
||||
'modernize_types_casting' => true,
|
||||
'multiline_comment_opening_closing' => true,
|
||||
'native_function_casing' => true,
|
||||
'native_function_type_declaration_casing' => true,
|
||||
'new_with_braces' => true,
|
||||
'no_blank_lines_after_class_opening' => true,
|
||||
'no_blank_lines_after_phpdoc' => true,
|
||||
'no_empty_comment' => true,
|
||||
'no_empty_phpdoc' => true,
|
||||
'no_empty_statement' => true,
|
||||
'no_leading_namespace_whitespace' => true,
|
||||
'no_mixed_echo_print' => ['use' => 'echo'],
|
||||
'no_unneeded_control_parentheses' => true,
|
||||
'no_unused_imports' => true,
|
||||
'no_useless_return' => true,
|
||||
'ordered_imports' => ['sort_algorithm' => 'alpha'],
|
||||
// 'phpdoc_add_missing_param_annotation' => ['only_untyped' => false], // adds params in the wrong order
|
||||
'phpdoc_align' => ['align' => 'left'],
|
||||
'phpdoc_indent' => true,
|
||||
'phpdoc_scalar' => true,
|
||||
'phpdoc_trim' => true,
|
||||
'short_scalar_cast' => true,
|
||||
'single_line_comment_style' => true,
|
||||
'single_quote' => true,
|
||||
'ternary_to_null_coalescing' => true,
|
||||
'whitespace_after_comma_in_array' => true
|
||||
])
|
||||
->setRiskyAllowed(true)
|
||||
->setIndent("\t")
|
||||
->setFinder($finder);
|
||||
21
site/plugins/kirby-seo/LICENSE
Normal file
21
site/plugins/kirby-seo/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
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.
|
||||
30
site/plugins/kirby-seo/README.md
Normal file
30
site/plugins/kirby-seo/README.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||

|
||||
|
||||
<h1 align="center">Kirby SEO</h1>
|
||||
<p align="center">All-in-one toolkit that makes implementing SEO & Meta best practices a breeze</p>
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
## Get started
|
||||
|
||||
[Read the documentation](https://plugins.andkindness.com/seo/docs/get-started/feature-overview) to get started with Kirby SEO.
|
||||
|
||||
## Support the project
|
||||
|
||||
> [!NOTE]
|
||||
> This plugin is provided free of charge & published under the permissive MIT License. If you use it in a commercial project, please consider sponsoring me on GitHub to support further development and continued maintenance of Kirby SEO.
|
||||
|
||||
## License
|
||||
|
||||
[MIT License](./../LICENSE)
|
||||
Copyright © 2023-2024 Tobias Möritz
|
||||
33
site/plugins/kirby-seo/blueprints/fields/meta-group.yml
Normal file
33
site/plugins/kirby-seo/blueprints/fields/meta-group.yml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
type: group
|
||||
fields:
|
||||
metaHeadline:
|
||||
label: meta-headline
|
||||
type: headline
|
||||
numbered: false
|
||||
metaTitle:
|
||||
label: title-overwrite
|
||||
type: text
|
||||
placeholder: '{{ page.title }}'
|
||||
metaTemplate:
|
||||
label: meta-title-template
|
||||
type: text
|
||||
help: meta-title-template-help
|
||||
width: 2/3
|
||||
placeholder: '{{ page.metadata.metaTemplate }}'
|
||||
useTitleTemplate:
|
||||
label: use-title-template
|
||||
type: toggle
|
||||
help: use-title-template-help
|
||||
width: 1/3
|
||||
default: true
|
||||
text:
|
||||
- "{{ t('use-title-template-no') }}"
|
||||
- "{{ t('use-title-template-yes') }}"
|
||||
metaDescription:
|
||||
label: meta-description
|
||||
type: textarea
|
||||
help: meta-description-help
|
||||
placeholder: '{{ page.metadata.metaDescription }}'
|
||||
buttons: false
|
||||
seoLine1:
|
||||
type: line
|
||||
48
site/plugins/kirby-seo/blueprints/fields/og-group.yml
Normal file
48
site/plugins/kirby-seo/blueprints/fields/og-group.yml
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
type: group
|
||||
fields:
|
||||
ogHeadline:
|
||||
label: og-headline
|
||||
type: headline
|
||||
numbered: false
|
||||
help: global-og-headline-help
|
||||
ogTemplate:
|
||||
label: og-title-template
|
||||
type: text
|
||||
width: 2/3
|
||||
help: meta-title-template-help
|
||||
placeholder: '{{ page.metadata.ogTemplate }}'
|
||||
useOgTemplate:
|
||||
label: use-title-template
|
||||
type: toggle
|
||||
help: use-title-template-help
|
||||
width: 1/3
|
||||
default: true
|
||||
text:
|
||||
- "{{ t('use-title-template-no') }}"
|
||||
- "{{ t('use-title-template-yes') }}"
|
||||
ogDescription:
|
||||
label: og-description
|
||||
type: textarea
|
||||
buttons: false
|
||||
placeholder: '{{ page.metadata.ogDescription }}'
|
||||
ogImage:
|
||||
label: og-image
|
||||
extends: seo/fields/og-image
|
||||
empty: og-image-empty
|
||||
twitterCardType:
|
||||
label: twitter-card-type
|
||||
width: 1/2
|
||||
placeholder: "{{ t('default-select') }} {{ t(site.twitterCardType) }}"
|
||||
type: select
|
||||
options:
|
||||
summary: "{{ t('summary') }}"
|
||||
summary_large_image: "{{ t('summary_large_image') }}"
|
||||
help: twitter-card-type-help
|
||||
twitterAuthor:
|
||||
label: twitter-author
|
||||
width: 1/2
|
||||
type: text
|
||||
before: '@'
|
||||
placeholder: '{{ page.metadata.twitterCreator }}'
|
||||
seoLine2:
|
||||
type: line
|
||||
31
site/plugins/kirby-seo/blueprints/fields/og-image.php
Normal file
31
site/plugins/kirby-seo/blueprints/fields/og-image.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
return function (App $kirby) {
|
||||
$blueprint = [
|
||||
'type' => 'files',
|
||||
'multiple' => false,
|
||||
'uploads' => [],
|
||||
'query' => Str::contains($kirby->path(), '/site') && !Str::contains($kirby->path(), 'pages') ? 'site.images' : 'page.images' // small hack to get context for field using api path
|
||||
];
|
||||
|
||||
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;
|
||||
};
|
||||
55
site/plugins/kirby-seo/blueprints/fields/robots.php
Normal file
55
site/plugins/kirby-seo/blueprints/fields/robots.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Str;
|
||||
use tobimori\Seo\Meta;
|
||||
|
||||
return function (App $kirby) {
|
||||
if (!$kirby->option('tobimori.seo.robots.active') || !$kirby->option('tobimori.seo.robots.pageSettings')) {
|
||||
return [
|
||||
'type' => 'hidden'
|
||||
];
|
||||
}
|
||||
|
||||
$fields = [
|
||||
'robotsHeadline' => [
|
||||
'label' => 'robots',
|
||||
'type' => 'headline',
|
||||
'numbered' => false,
|
||||
]
|
||||
];
|
||||
|
||||
$page = Meta::currentPage();
|
||||
foreach ($kirby->option('tobimori.seo.robots.types') as $robots) {
|
||||
$upper = Str::ucfirst($robots);
|
||||
|
||||
$fields["robots{$upper}"] = [
|
||||
'label' => "robots-{$robots}",
|
||||
'type' => 'toggles',
|
||||
'help' => "robots-{$robots}-help",
|
||||
'width' => '1/2',
|
||||
'default' => 'default',
|
||||
'reset' => false,
|
||||
'options' => [
|
||||
'default' => $page ?
|
||||
A::join([
|
||||
t('default-select'),
|
||||
$page->metadata()->get("robots{$upper}", ['fields'])->toBool() ? t('yes') : t('no')
|
||||
], ' ')
|
||||
: t('default-select'),
|
||||
'true' => t('yes'),
|
||||
'false' => t('no'),
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
$fields['seoLine3'] = [
|
||||
'type' => 'line'
|
||||
];
|
||||
|
||||
return [
|
||||
'type' => 'group',
|
||||
'fields' => $fields,
|
||||
];
|
||||
};
|
||||
49
site/plugins/kirby-seo/blueprints/fields/site-robots.php
Normal file
49
site/plugins/kirby-seo/blueprints/fields/site-robots.php
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Cms\App;
|
||||
|
||||
return function (App $kirby) {
|
||||
if (!$kirby->option('tobimori.seo.robots.active') || !$kirby->option('tobimori.seo.robots.pageSettings')) {
|
||||
return [
|
||||
'type' => 'hidden'
|
||||
];
|
||||
}
|
||||
|
||||
$fields = [
|
||||
'robotsHeadline' => [
|
||||
'label' => 'robots',
|
||||
'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' => "robots-{$robots}",
|
||||
'type' => 'toggles',
|
||||
'help' => "robots-{$robots}-help",
|
||||
'width' => '1/2',
|
||||
'default' => 'default',
|
||||
'reset' => false,
|
||||
'options' => [
|
||||
'default' => t('default-select') . ' ' . ($index ? t('yes') : t('no')),
|
||||
'true' => t('yes'),
|
||||
'false' => t('no'),
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
$fields['seoLine3'] = [
|
||||
'type' => 'line'
|
||||
];
|
||||
|
||||
return [
|
||||
'type' => 'group',
|
||||
'fields' => $fields,
|
||||
];
|
||||
};
|
||||
30
site/plugins/kirby-seo/blueprints/fields/social-media.php
Normal file
30
site/plugins/kirby-seo/blueprints/fields/social-media.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Social Media Accounts field
|
||||
* Allows social media account list to be filled by config options
|
||||
*/
|
||||
|
||||
use Kirby\Cms\App;
|
||||
|
||||
return function (App $kirby) {
|
||||
$fields = [];
|
||||
|
||||
foreach ($kirby->option('tobimori.seo.socialMedia') as $key => $value) {
|
||||
if ($value) {
|
||||
$fields[$key] = [
|
||||
'label' => ucfirst($key),
|
||||
'type' => 'url',
|
||||
'icon' => strtolower($key),
|
||||
'placeholder' => $value
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'label' => 'social-media-accounts',
|
||||
'type' => 'object',
|
||||
'help' => 'social-media-accounts-help',
|
||||
'fields' => $fields
|
||||
];
|
||||
};
|
||||
29
site/plugins/kirby-seo/blueprints/page.yml
Normal file
29
site/plugins/kirby-seo/blueprints/page.yml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
label: metadata-site
|
||||
icon: search
|
||||
|
||||
columns:
|
||||
main:
|
||||
width: 7/12
|
||||
fields:
|
||||
metaGroup: seo/fields/meta-group
|
||||
ogGroup: seo/fields/og-group
|
||||
robots: seo/fields/robots
|
||||
metaInherit:
|
||||
label: inherit-settings
|
||||
type: multiselect
|
||||
help: inherit-settings-help
|
||||
options:
|
||||
metaTemplate: "{{ t('meta-title-template') }}"
|
||||
metaDescription: "{{ t('meta-description') }}"
|
||||
ogTemplate: "{{ t('og-title-template') }}"
|
||||
ogDescription: "{{ t('og-description') }}"
|
||||
ogImage: "{{ t('og-image') }}"
|
||||
twitterCardType: "{{ t('twitter-card-type') }}"
|
||||
twitterAuthor: "{{ t('twitter-author') }}"
|
||||
robots: '{{ t("robots") }}'
|
||||
sidebar:
|
||||
width: 5/12
|
||||
sticky: true
|
||||
sections:
|
||||
seoPreview:
|
||||
type: seo-preview
|
||||
70
site/plugins/kirby-seo/blueprints/site.yml
Normal file
70
site/plugins/kirby-seo/blueprints/site.yml
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
label: metadata-site
|
||||
icon: search
|
||||
|
||||
columns:
|
||||
main:
|
||||
width: 7/12
|
||||
fields:
|
||||
metaHeadline:
|
||||
label: global-meta-headline
|
||||
type: headline
|
||||
numbered: false
|
||||
help: global-meta-headline-help
|
||||
metaTemplate:
|
||||
label: meta-title-template
|
||||
type: text
|
||||
help: meta-title-template-help
|
||||
metaDescription:
|
||||
label: meta-description
|
||||
type: textarea
|
||||
help: meta-description-help
|
||||
buttons: false
|
||||
seoLine1:
|
||||
type: line
|
||||
ogHeadline:
|
||||
label: global-og-headline
|
||||
type: headline
|
||||
numbered: false
|
||||
help: global-og-headline-help
|
||||
ogTemplate:
|
||||
label: og-title-template
|
||||
type: text
|
||||
default: '{{ title }}'
|
||||
help: meta-title-template-help
|
||||
placeholder: '{{ site.metaTemplate }}'
|
||||
ogDescription:
|
||||
label: og-description
|
||||
type: textarea
|
||||
buttons: false
|
||||
placeholder: '{{ site.metaDescription }}'
|
||||
ogSiteName:
|
||||
label: og-site-name
|
||||
type: text
|
||||
default: '{{ site.title }}'
|
||||
placeholder: '{{ site.title }}'
|
||||
width: 1/2
|
||||
ogImage:
|
||||
label: og-image
|
||||
extends: seo/fields/og-image
|
||||
empty: og-image-empty
|
||||
width: 1/2
|
||||
twitterCardType:
|
||||
label: twitter-card-type
|
||||
width: 1/2
|
||||
type: select
|
||||
default: summary
|
||||
required: true
|
||||
options:
|
||||
summary: "{{ t('summary') }}"
|
||||
summary_large_image: "{{ t('summary_large_image') }}"
|
||||
help: twitter-card-type-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
|
||||
670
site/plugins/kirby-seo/classes/Meta.php
Normal file
670
site/plugins/kirby-seo/classes/Meta.php
Normal file
|
|
@ -0,0 +1,670 @@
|
|||
<?php
|
||||
|
||||
namespace tobimori\Seo;
|
||||
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Cms\Page;
|
||||
use Kirby\Content\Field;
|
||||
use Kirby\Exception\InvalidArgumentException;
|
||||
use Kirby\Toolkit\A;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
/**
|
||||
* The Meta class is responsible for handling the meta data & cascading
|
||||
*/
|
||||
class Meta
|
||||
{
|
||||
/**
|
||||
* These values will be handled as 'field is empty'
|
||||
*/
|
||||
public const DEFAULT_VALUES = ['[]', 'default'];
|
||||
|
||||
protected Page $page;
|
||||
protected ?string $lang;
|
||||
protected array $consumed = [];
|
||||
protected array $metaDefaults = [];
|
||||
protected array $metaArray = [];
|
||||
|
||||
/**
|
||||
* Creates a new Meta instance
|
||||
*/
|
||||
public function __construct(Page $page, ?string $lang = null)
|
||||
{
|
||||
$this->page = $page;
|
||||
$this->lang = $lang;
|
||||
|
||||
if (method_exists($this->page, 'metaDefaults')) {
|
||||
$this->metaDefaults = $this->page->metaDefaults($this->lang);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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->ogImage() ? $this->get('ogImage')->toFile()?->width() : null,
|
||||
'og:image:height' => fn () => $this->ogImage() ? $this->get('ogImage')->toFile()?->height() : null,
|
||||
'og:image:alt' => fn () => $this->get('ogImage')->toFile()?->alt(),
|
||||
'og:type' => 'ogType',
|
||||
];
|
||||
|
||||
|
||||
// Robots
|
||||
if ($robotsActive = option('tobimori.seo.robots.active')) {
|
||||
$meta['robots'] = fn () => $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;
|
||||
|
||||
// Multi-lang alternate tags
|
||||
if (kirby()->languages()->count() > 1 && kirby()->language() !== null) {
|
||||
foreach (kirby()->languages() as $lang) {
|
||||
// only add alternate tags if the page is indexable
|
||||
$meta['alternate'][] = fn () => $allowsIndexFn() ? [
|
||||
'hreflang' => $lang->code(),
|
||||
'href' => $this->page->url($lang->code()),
|
||||
] : null;
|
||||
|
||||
if ($lang !== kirby()->language()) {
|
||||
$meta['og:locale:alternate'][] = fn () => $lang->code();
|
||||
}
|
||||
}
|
||||
|
||||
// only add alternate tags if the page is indexable
|
||||
$meta['alternate'][] = fn () => $allowsIndexFn() ? [
|
||||
'hreflang' => 'x-default',
|
||||
'href' => $this->page->url(kirby()->language()->code()),
|
||||
] : null;
|
||||
$meta['og:locale'] = fn () => kirby()->language()->locale(LC_ALL);
|
||||
} else {
|
||||
$meta['og:locale'] = fn () => $this->locale(LC_ALL);
|
||||
}
|
||||
|
||||
// Twitter tags "opt-in" - TODO: wip
|
||||
if (option('tobimori.seo.twitter', true)) {
|
||||
$meta = array_merge($meta, [
|
||||
'twitter:card' => 'twitterCardType',
|
||||
'twitter:title' => 'ogTitle',
|
||||
'twitter:description' => 'ogDescription',
|
||||
'twitter:image' => 'ogImage',
|
||||
'twitter:site' => 'twitterSite',
|
||||
'twitter:creator' => 'twitterCreator',
|
||||
]);
|
||||
}
|
||||
|
||||
// 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',
|
||||
'tags' => [
|
||||
'title'
|
||||
]
|
||||
],
|
||||
[
|
||||
'tag' => 'link',
|
||||
'attributes' => [
|
||||
'name' => 'rel',
|
||||
'content' => 'href',
|
||||
],
|
||||
'tags' => [
|
||||
'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, 'Kirby\Content\Field') && $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,
|
||||
];
|
||||
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,
|
||||
];
|
||||
}
|
||||
|
||||
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 = option('tobimori.seo.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)) {
|
||||
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)->get($key))) {
|
||||
if (Str::contains($key, 'robots') && !option('tobimori.seo.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 = option('tobimori.seo.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)->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 = option("tobimori.seo.default.{$key}")) {
|
||||
if (is_callable($option)) {
|
||||
$option = $option($this->page);
|
||||
}
|
||||
|
||||
if (is_a($option, 'Kirby\Content\Field')) {
|
||||
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());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Twitter username from an account url set in the site options
|
||||
*/
|
||||
public function twitterSite()
|
||||
{
|
||||
$accs = $this->page->site()->socialMediaAccounts()->toObject();
|
||||
$username = '';
|
||||
|
||||
if ($accs->twitter()->isNotEmpty()) {
|
||||
// tries to match all twitter urls, and extract the username
|
||||
$matches = [];
|
||||
preg_match('/^(https?:\/\/)?(www\.)?twitter\.com\/(#!\/)?@?(?<name>[^\/\?]*)$/', $accs->twitter()->value(), $matches);
|
||||
if (isset($matches['name'])) {
|
||||
$username = $matches['name'];
|
||||
}
|
||||
}
|
||||
|
||||
return new Field($this->page, 'twitter', $username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the date format for modified meta tags, based on the registered date handler
|
||||
*/
|
||||
public function dateFormat(): string
|
||||
{
|
||||
if ($custom = option('tobimori.seo.dateFormat')) {
|
||||
if (is_callable($custom)) {
|
||||
return $custom($this->page);
|
||||
}
|
||||
|
||||
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 (option('tobimori.seo.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 url
|
||||
*/
|
||||
public function ogImage(): string|null
|
||||
{
|
||||
$field = $this->get('ogImage');
|
||||
|
||||
if ($ogImage = $field->toFile()?->thumb([
|
||||
'width' => 1200,
|
||||
'height' => 630,
|
||||
'crop' => true,
|
||||
])) {
|
||||
return $ogImage->url();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
29
site/plugins/kirby-seo/classes/SchemaSingleton.php
Normal file
29
site/plugins/kirby-seo/classes/SchemaSingleton.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace tobimori\Seo;
|
||||
|
||||
use Kirby\Cms\Page;
|
||||
use Spatie\SchemaOrg\Schema;
|
||||
|
||||
class SchemaSingleton
|
||||
{
|
||||
private static $instances = [];
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public static function getInstance(string $type, Page|null $page = null): mixed
|
||||
{
|
||||
if (!isset(self::$instances[$page?->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
|
||||
{
|
||||
return self::$instances[$page?->id() ?? 'default'] ?? [];
|
||||
}
|
||||
}
|
||||
82
site/plugins/kirby-seo/classes/Sitemap/Sitemap.php
Normal file
82
site/plugins/kirby-seo/classes/Sitemap/Sitemap.php
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
namespace tobimori\Seo\Sitemap;
|
||||
|
||||
use DOMDocument;
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Toolkit\Collection;
|
||||
|
||||
class Sitemap extends Collection
|
||||
{
|
||||
public function __construct(protected string $key, array $data = [], bool $caseSensitive = false)
|
||||
{
|
||||
parent::__construct($data, $caseSensitive);
|
||||
}
|
||||
|
||||
public function key(): string
|
||||
{
|
||||
return $this->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;
|
||||
|
||||
$doc->appendChild($doc->createProcessingInstruction('xml-stylesheet', 'type="text/xsl" href="/sitemap.xsl"'));
|
||||
|
||||
$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');
|
||||
$root->setAttribute('seo-version', App::plugin('tobimori/seo')->version());
|
||||
|
||||
foreach ($this as $url) {
|
||||
$root->appendChild($url->toDOMNode($doc));
|
||||
}
|
||||
|
||||
$doc->appendChild($root);
|
||||
return $doc->saveXML();
|
||||
}
|
||||
}
|
||||
100
site/plugins/kirby-seo/classes/Sitemap/SitemapIndex.php
Normal file
100
site/plugins/kirby-seo/classes/Sitemap/SitemapIndex.php
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
|
||||
namespace tobimori\Seo\Sitemap;
|
||||
|
||||
use DOMDocument;
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Cms\Page;
|
||||
use Kirby\Toolkit\Collection;
|
||||
|
||||
class SitemapIndex extends Collection
|
||||
{
|
||||
protected static $instance = null;
|
||||
|
||||
public static function instance(...$args): static
|
||||
{
|
||||
if (static::$instance === null) {
|
||||
static::$instance = new static(...$args);
|
||||
}
|
||||
|
||||
return static::$instance;
|
||||
}
|
||||
|
||||
public function create(string $key = 'pages'): Sitemap
|
||||
{
|
||||
$sitemap = $this->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;
|
||||
|
||||
$doc->appendChild($doc->createProcessingInstruction('xml-stylesheet', 'type="text/xsl" href="sitemap.xsl"'));
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
113
site/plugins/kirby-seo/classes/Sitemap/SitemapUrl.php
Normal file
113
site/plugins/kirby-seo/classes/Sitemap/SitemapUrl.php
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
|
||||
namespace tobimori\Seo\Sitemap;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMNode;
|
||||
use Kirby\Exception\Exception;
|
||||
|
||||
class SitemapUrl
|
||||
{
|
||||
protected string $lastmod;
|
||||
protected string $changefreq;
|
||||
protected string $priority;
|
||||
protected array $alternates = [];
|
||||
|
||||
public function __construct(protected string $loc)
|
||||
{
|
||||
}
|
||||
|
||||
public function loc(string $url = null): SitemapUrl|string
|
||||
{
|
||||
if ($url === null) {
|
||||
return $this->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);
|
||||
}
|
||||
}
|
||||
41
site/plugins/kirby-seo/composer.json
Normal file
41
site/plugins/kirby-seo/composer.json
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"name": "tobimori/kirby-seo",
|
||||
"description": "The ultimate Kirby SEO toolkit",
|
||||
"type": "kirby-plugin",
|
||||
"version": "1.1.2",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/tobimori/kirby-seo#readme",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Tobias Möritz",
|
||||
"email": "tobias@moeritz.io"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"tobimori\\Seo\\": "classes"
|
||||
}
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1.0",
|
||||
"getkirby/composer-installer": "^1.2.1",
|
||||
"spatie/schema-org": "^3.14"
|
||||
},
|
||||
"require-dev": {
|
||||
"getkirby/cms": "^4.0",
|
||||
"getkirby/cli": "^1.2",
|
||||
"friendsofphp/php-cs-fixer": "^3.48"
|
||||
},
|
||||
"scripts": {
|
||||
"dist": "composer install --no-dev --optimize-autoloader"
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"allow-plugins": {
|
||||
"getkirby/composer-installer": true
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"kirby-cms-path": false
|
||||
}
|
||||
}
|
||||
102
site/plugins/kirby-seo/config/api.php
Normal file
102
site/plugins/kirby-seo/config/api.php
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Cms\Page;
|
||||
use Kirby\Cms\Site;
|
||||
use Kirby\Form\Form;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
return [
|
||||
'data' => [
|
||||
'dirtyPageOrSite' => function (string $slug) {
|
||||
$kirby = kirby();
|
||||
$page = $slug == 'site' ? $kirby->site() : $kirby->page(Str::replace($slug, '+', '/'));
|
||||
|
||||
if ($this->requestBody()) {
|
||||
$form = Form::for($page, [ // Form class handles transformation of changed items
|
||||
'ignoreDisabled' => true,
|
||||
'input' => array_merge(['title' => $page->title()], $page->content()->data(), $this->requestBody()),
|
||||
'language' => $kirby->language()?->code()
|
||||
]);
|
||||
|
||||
$page = $page->clone(['content' => $form->data()]);
|
||||
}
|
||||
|
||||
return $page;
|
||||
}
|
||||
],
|
||||
'routes' => [
|
||||
[
|
||||
'pattern' => '/k-seo/(:any)/heading-structure',
|
||||
'method' => 'POST',
|
||||
'action' => function (string $slug) {
|
||||
$model = $this->dirtyPageOrSite($slug);
|
||||
|
||||
if ($model instanceof 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;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
],
|
||||
[
|
||||
'pattern' => '/k-seo/(:any)/seo-preview',
|
||||
'method' => 'POST',
|
||||
'action' => function (string $slug) {
|
||||
$model = $this->dirtyPageOrSite($slug);
|
||||
|
||||
if ($model instanceof Site) {
|
||||
$model = $model->homePage();
|
||||
}
|
||||
|
||||
if ($model instanceof Page) {
|
||||
$meta = $model->metadata();
|
||||
|
||||
return [
|
||||
'page' => $model->slug(),
|
||||
'url' => $model->url(),
|
||||
'title' => $meta->metaTitle()->value(),
|
||||
'description' => $meta->metaDescription()->value(),
|
||||
'ogSiteName' => $meta->ogSiteName()->value(),
|
||||
'ogTitle' => $meta->ogTitle()->value(),
|
||||
'ogDescription' => $meta->ogDescription()->value(),
|
||||
'ogImage' => $meta->ogImage(),
|
||||
'twitterCardType' => $meta->twitterCardType()->value(),
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
],
|
||||
[
|
||||
'pattern' => '/k-seo/(:any)/robots',
|
||||
'method' => 'POST',
|
||||
'action' => function (string $slug) {
|
||||
$model = $this->dirtyPageOrSite($slug);
|
||||
if (!($model instanceof Page)) {
|
||||
return null;
|
||||
}
|
||||
$robots = $model->robots();
|
||||
|
||||
return [
|
||||
'active' => option('tobimori.seo.robots.indicator', option('tobimori.seo.robots.active', true)),
|
||||
'state' => $robots,
|
||||
];
|
||||
}
|
||||
]
|
||||
]
|
||||
];
|
||||
11
site/plugins/kirby-seo/config/commands/hello.php
Normal file
11
site/plugins/kirby-seo/config/commands/hello.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
use Kirby\CLI\CLI;
|
||||
|
||||
return [
|
||||
'description' => 'Hello world',
|
||||
'args' => [],
|
||||
'command' => static function (CLI $cli): void {
|
||||
$cli->success('Hello world! This command is a preparation for a future release.');
|
||||
}
|
||||
];
|
||||
27
site/plugins/kirby-seo/config/hooks.php
Normal file
27
site/plugins/kirby-seo/config/hooks.php
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Cms\Page;
|
||||
use Kirby\Toolkit\Str;
|
||||
|
||||
return [
|
||||
'page.update:after' => function (Page $newPage, Page $oldPage) {
|
||||
foreach ($newPage->kirby()->option('tobimori.seo.robots.types') as $robots) {
|
||||
$upper = Str::ucfirst($robots);
|
||||
if ($newPage->content()->get("robots{$upper}")->value() === "") {
|
||||
$newPage = $newPage->update([
|
||||
"robots{$upper}" => 'default'
|
||||
]);
|
||||
}
|
||||
}
|
||||
},
|
||||
'page.render:before' => function (string $contentType, array $data, Page $page) {
|
||||
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());
|
||||
}
|
||||
},
|
||||
];
|
||||
82
site/plugins/kirby-seo/config/options.php
Normal file
82
site/plugins/kirby-seo/config/options.php
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Cms\Page;
|
||||
|
||||
return [
|
||||
'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',
|
||||
'twitterCardType' => 'summary',
|
||||
'ogDescription' => fn (Page $page) => $page->metadata()->metaDescription(),
|
||||
'twitterCreator' => fn (Page $page) => $page->metadata()->twitterSite(),
|
||||
'lang' => fn (Page $page) => $page->kirby()->language()?->locale(LC_ALL) ?? $page->kirby()->option('tobimori.seo.lang', 'en_US'),
|
||||
// default for robots: noIndex if global index configuration is set, otherwise fall back to page status
|
||||
'robotsIndex' => function (Page $page) {
|
||||
$index = $page->kirby()->option('tobimori.seo.robots.index');
|
||||
if (is_callable($index)) {
|
||||
$index = $index();
|
||||
}
|
||||
|
||||
if (!$index) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $page->kirby()->option('tobimori.seo.robots.followPageStatus', true) ? $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',
|
||||
],
|
||||
'previews' => [
|
||||
'google',
|
||||
'facebook',
|
||||
'slack'
|
||||
],
|
||||
'robots' => [
|
||||
'active' => true, // whether robots handling should be done by the plugin
|
||||
'followPageStatus' => true, // should unlisted pages be noindex by default?
|
||||
'pageSettings' => true, // whether to have robots settings on each page
|
||||
'indicator' => true, // whether the indicator should be shown in the panel
|
||||
'index' => fn () => !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' => [
|
||||
'active' => true,
|
||||
'redirect' => true, // redirect /sitemap to /sitemap.xml
|
||||
'lang' => 'en',
|
||||
'generator' => require __DIR__ . '/options/sitemap.php',
|
||||
'changefreq' => 'weekly',
|
||||
'groupByTemplate' => false,
|
||||
'excludeTemplates' => ['error'],
|
||||
'priority' => fn (Page $p) => number_format(($p->isHomePage()) ? 1 : max(1 - 0.2 * $p->depth(), 0.2), 1),
|
||||
],
|
||||
'files' => [
|
||||
'parent' => null,
|
||||
'template' => null,
|
||||
],
|
||||
'canonicalBase' => null, // base url for canonical links
|
||||
'generateSchema' => true, // whether to generate default schema.org data
|
||||
'lang' => 'en_US', // default language, used for single-language sites
|
||||
'dateFormat' => null, // custom date format
|
||||
];
|
||||
37
site/plugins/kirby-seo/config/options/sitemap.php
Normal file
37
site/plugins/kirby-seo/config/options/sitemap.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Toolkit\Obj;
|
||||
use tobimori\Seo\Sitemap\SitemapIndex;
|
||||
|
||||
return function (SitemapIndex $sitemap) {
|
||||
$exclude = option('tobimori.seo.sitemap.excludeTemplates', []);
|
||||
$pages = site()->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) {
|
||||
$url->alternates(
|
||||
kirby()->languages()->map(fn ($language) => new Obj([
|
||||
'hreflang' => $language->code() === kirby()->language()->code() ? 'x-default' : $language->code(),
|
||||
'href' => $page->url($language->code()),
|
||||
]))->toArray()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
11
site/plugins/kirby-seo/config/pageMethods.php
Normal file
11
site/plugins/kirby-seo/config/pageMethods.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
use tobimori\Seo\Meta;
|
||||
use tobimori\Seo\SchemaSingleton;
|
||||
|
||||
return [
|
||||
'schema' => fn ($type) => SchemaSingleton::getInstance($type, $this),
|
||||
'schemas' => fn () => SchemaSingleton::getInstances($this),
|
||||
'metadata' => fn (?string $lang = null) => new Meta($this, $lang),
|
||||
'robots' => fn (?string $lang = null) => $this->metadata($lang)->robots(),
|
||||
];
|
||||
98
site/plugins/kirby-seo/config/routes.php
Normal file
98
site/plugins/kirby-seo/config/routes.php
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Cms\Page;
|
||||
use Kirby\Http\Response;
|
||||
use tobimori\Seo\Sitemap\SitemapIndex;
|
||||
|
||||
return [
|
||||
[
|
||||
'pattern' => 'robots.txt',
|
||||
'action' => function () {
|
||||
if (option('tobimori.seo.robots.active', true)) {
|
||||
$content = snippet('seo/robots.txt', [], true);
|
||||
return new Response($content, 'text/plain', 200);
|
||||
}
|
||||
|
||||
$this->next();
|
||||
}
|
||||
],
|
||||
[
|
||||
'pattern' => 'sitemap',
|
||||
'action' => function () {
|
||||
if (!option('tobimori.seo.sitemap.redirect', true) || !option('tobimori.seo.sitemap.active', true)) {
|
||||
$this->next();
|
||||
}
|
||||
|
||||
go('/sitemap.xml');
|
||||
}
|
||||
],
|
||||
[
|
||||
'pattern' => 'sitemap.xsl',
|
||||
'action' => function () {
|
||||
if (!option('tobimori.seo.sitemap.active', true)) {
|
||||
$this->next();
|
||||
}
|
||||
|
||||
kirby()->response()->type('text/xsl');
|
||||
|
||||
$lang = option('tobimori.seo.sitemap.lang', 'en');
|
||||
if (is_callable($lang)) {
|
||||
$lang = $lang();
|
||||
}
|
||||
kirby()->setCurrentTranslation($lang);
|
||||
|
||||
return Page::factory([
|
||||
'slug' => 'sitemap',
|
||||
'template' => 'sitemap',
|
||||
'model' => 'sitemap',
|
||||
'content' => [
|
||||
'title' => t('sitemap'),
|
||||
],
|
||||
])->render(contentType: 'xsl');
|
||||
}
|
||||
],
|
||||
[
|
||||
'pattern' => 'sitemap.xml',
|
||||
'action' => function () {
|
||||
if (!option('tobimori.seo.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-(:any).xml',
|
||||
'action' => function (string $index) {
|
||||
if (!option('tobimori.seo.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');
|
||||
}
|
||||
]
|
||||
];
|
||||
20
site/plugins/kirby-seo/config/sections.php
Normal file
20
site/plugins/kirby-seo/config/sections.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Toolkit\A;
|
||||
|
||||
return [
|
||||
'seo-preview' => [
|
||||
'mixins' => ['headline'],
|
||||
'computed' => [
|
||||
'options' => function () {
|
||||
return A::map(option('tobimori.seo.previews'), fn ($item) => [
|
||||
'value' => $item,
|
||||
'text' => t($item)
|
||||
]);
|
||||
}
|
||||
]
|
||||
],
|
||||
'heading-structure' => [
|
||||
'mixins' => ['headline']
|
||||
]
|
||||
];
|
||||
28
site/plugins/kirby-seo/config/siteMethods.php
Normal file
28
site/plugins/kirby-seo/config/siteMethods.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Http\Url;
|
||||
use Kirby\Toolkit\Str;
|
||||
use tobimori\Seo\SchemaSingleton;
|
||||
|
||||
return [
|
||||
'schema' => fn ($type) => SchemaSingleton::getInstance($type),
|
||||
'schemas' => fn () => SchemaSingleton::getInstances(),
|
||||
'lang' => fn () => Str::replace(option('tobimori.seo.default.lang')($this->homePage()), '_', '-'),
|
||||
'canonicalFor' => function (string $url) {
|
||||
$base = option('tobimori.seo.canonicalBase');
|
||||
if (is_callable($base)) {
|
||||
$base = $base($url);
|
||||
}
|
||||
|
||||
if ($base === null) {
|
||||
$base = $this->url(); // graceful fallback to site url
|
||||
}
|
||||
|
||||
if (Str::startsWith($url, $base)) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$path = Url::path($url);
|
||||
return url($base . '/' . $path);
|
||||
}
|
||||
];
|
||||
1
site/plugins/kirby-seo/index.css
Normal file
1
site/plugins/kirby-seo/index.css
Normal file
File diff suppressed because one or more lines are too long
1
site/plugins/kirby-seo/index.js
Normal file
1
site/plugins/kirby-seo/index.js
Normal file
File diff suppressed because one or more lines are too long
69
site/plugins/kirby-seo/index.php
Normal file
69
site/plugins/kirby-seo/index.php
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
@include_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use Kirby\Cms\App;
|
||||
use Kirby\Data\Yaml;
|
||||
use Kirby\Filesystem\Dir;
|
||||
use Kirby\Filesystem\F;
|
||||
use Kirby\Toolkit\A;
|
||||
use Spatie\SchemaOrg\Schema;
|
||||
|
||||
// shamelessly borrowed from distantnative/retour-for-kirby
|
||||
if (
|
||||
version_compare(App::version() ?? '0.0.0', '4.0.2', '<') === true ||
|
||||
version_compare(App::version() ?? '0.0.0', '5.0.0', '>') === true
|
||||
) {
|
||||
throw new Exception('Kirby SEO requires Kirby 4.0.2 or higher.');
|
||||
}
|
||||
|
||||
App::plugin('tobimori/seo', [
|
||||
'options' => require __DIR__ . '/config/options.php',
|
||||
'sections' => require __DIR__ . '/config/sections.php',
|
||||
'api' => require __DIR__ . '/config/api.php',
|
||||
'siteMethods' => require __DIR__ . '/config/siteMethods.php',
|
||||
'pageMethods' => require __DIR__ . '/config/pageMethods.php',
|
||||
'hooks' => require __DIR__ . '/config/hooks.php',
|
||||
'routes' => require __DIR__ . '/config/routes.php',
|
||||
// load all commands automatically
|
||||
'commands' => A::keyBy(A::map(
|
||||
Dir::read(__DIR__ . '/config/commands'),
|
||||
fn ($file) => A::merge([
|
||||
'id' => 'seo:' . F::name($file),
|
||||
], require __DIR__ . '/config/commands/' . $file)
|
||||
), 'id'),
|
||||
// get all files from /translations and register them as language files
|
||||
'translations' => A::keyBy(A::map(
|
||||
Dir::read(__DIR__ . '/translations'),
|
||||
fn ($file) => A::merge([
|
||||
'lang' => F::name($file),
|
||||
], Yaml::decode(F::read(__DIR__ . '/translations/' . $file)))
|
||||
), 'lang'),
|
||||
'snippets' => [
|
||||
'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/site' => __DIR__ . '/blueprints/site.yml',
|
||||
'seo/page' => __DIR__ . '/blueprints/page.yml',
|
||||
'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/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',
|
||||
],
|
||||
]);
|
||||
|
||||
if (!function_exists('schema')) {
|
||||
function schema($type)
|
||||
{
|
||||
return Schema::{$type}();
|
||||
}
|
||||
}
|
||||
17
site/plugins/kirby-seo/package.json
Normal file
17
site/plugins/kirby-seo/package.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"author": "Tobias Möritz",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "kirbyup serve src/index.js",
|
||||
"build": "kirbyup src/index.js",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"husky": "^9.0.11",
|
||||
"kirbyup": "^3.1.4",
|
||||
"postcss": "^8.4.35",
|
||||
"sass": "^1.71.1"
|
||||
}
|
||||
}
|
||||
6
site/plugins/kirby-seo/postcss.config.js
Normal file
6
site/plugins/kirby-seo/postcss.config.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
'postcss-logical': {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
||||
13
site/plugins/kirby-seo/snippets/head.php
Normal file
13
site/plugins/kirby-seo/snippets/head.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @var \Kirby\Cms\Page $page
|
||||
*/
|
||||
|
||||
use Kirby\Cms\Html;
|
||||
|
||||
$tags = $page->metadata()->snippetData();
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
echo Html::tag($tag['tag'], $tag['content'] ?? null, $tag['attributes'] ?? []) . PHP_EOL;
|
||||
}
|
||||
58
site/plugins/kirby-seo/snippets/robots.txt.php
Normal file
58
site/plugins/kirby-seo/snippets/robots.txt.php
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
use Kirby\Toolkit\A;
|
||||
|
||||
if ($content = option('tobimori.seo.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 = option('tobimori.seo.robots.index');
|
||||
if (is_callable($index)) {
|
||||
$index = $index();
|
||||
}
|
||||
|
||||
if ($index) {
|
||||
echo 'Allow: /';
|
||||
echo "\nDisallow: /panel";
|
||||
} else {
|
||||
echo 'Disallow: /';
|
||||
}
|
||||
}
|
||||
|
||||
if (($sitemap = option('tobimori.seo.robots.sitemap')) || ($sitemapModule = option('tobimori.seo.sitemap.active'))) {
|
||||
// Allow closure to be used
|
||||
if (is_callable($sitemap)) {
|
||||
$sitemap = $sitemap();
|
||||
}
|
||||
|
||||
// Use default sitemap if none is set
|
||||
if (!$sitemap && $sitemapModule) {
|
||||
$sitemap = site()->canonicalFor('/sitemap.xml');
|
||||
}
|
||||
|
||||
// Check again, so falsy values can't be used
|
||||
if ($sitemap) {
|
||||
echo "\n\nSitemap: {$sitemap}";
|
||||
}
|
||||
}
|
||||
8
site/plugins/kirby-seo/snippets/schemas.php
Normal file
8
site/plugins/kirby-seo/snippets/schemas.php
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
$siteSchema ??= true;
|
||||
$pageSchema ??= true;
|
||||
|
||||
foreach (array_merge($siteSchema ? $site->schemas() : [], $pageSchema ? $page->schemas() : []) as $schema) {
|
||||
echo $schema;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue