Compare commits

..

No commits in common. "main" and "simplify" have entirely different histories.

141 changed files with 1342 additions and 14628 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

View file

@ -8,18 +8,3 @@
.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;
}

View file

@ -1,39 +1,8 @@
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) {

View file

@ -1,72 +1,36 @@
#main-footer {
position: fixed;
bottom: 0;
width: 100%;
box-sizing: border-box;
border-bottom: 0;
padding: var(--unit--horizontal);
}
[data-template="home"] #main-footer {
position: fixed;
left: 0;
bottom: 0;
}
#main-footer li:not(.open-nav-wrapper) {
display: none;
#main-footer ul {
display: flex;
justify-content: center;
gap: calc(2 * var(--unit--horizontal));
}
#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);
}
#main-footer a {
text-shadow: 0 0 2px #000;
-moz-text-shadow: 0 0 2px #000;
-webkit-text-shadow: 0 0 2px #000;
}
@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;
}
}

View file

@ -74,18 +74,6 @@ body {
opacity: var(--opacity-light);
}
.footnote::before {
content: "[";
}
.footnote::after {
content: "]";
}
/* ================= COLORS ================= */
.texts .text {
margin-bottom: var(--unit--vertical);
}
/* ================= COLORS ================= */
.color {
color: var(--color);
@ -96,8 +84,7 @@ body {
/* ================= BUTTONS ================= */
.toggle-btn--left::after,
button.plus::after,
button.less::after {
button.plus::after {
margin-left: var(--unit--horizontal);
}
@ -105,8 +92,7 @@ button.less::after {
button.plus::after {
content: "+";
}
.toggle-btn--left.open::after,
button.less::after {
.toggle-btn--left.open::after {
content: "-";
}
.toggle-btn--right::before {
@ -122,19 +108,3 @@ button.less::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);
}

View file

@ -13,6 +13,13 @@ 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);
}
@ -20,26 +27,3 @@ 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);
}
}

View file

@ -1,29 +1,12 @@
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: 25.3vw;
font-size: 26.65vw;
font-weight: var(--font-weight-extra-bold);
}
@ -31,7 +14,7 @@ main article > div {
height: 20vw;
width: 100%;
box-sizing: border-box;
padding-right: 3vw;
padding-right: 1vw;
display: flex;
justify-content: flex-end;
align-items: center;
@ -41,10 +24,6 @@ main article > div {
text-align: right;
}
#logo #actuel {
font-weight: 550;
}
#logo #actuel,
#logo #inactuel {
mix-blend-mode: difference;
@ -56,91 +35,82 @@ main article > div {
#main-header.minimized #inactuel {
margin-top: -20vw;
transform: translateX(-1px) translateY(-1px);
transform: translateX(-2px) translateY(-2px);
}
#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: "+";
}
.page-cover .links {
position: absolute;
bottom: 8px;
width: 100%;
box-sizing: border-box;
#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 li {
display: inline-block;
margin-right: var(--unit--horizontal);
#entry-btns.minimized .entry-btn::before,
#entry-btns.minimized .entry-btn::after {
font-weight: bold;
}
[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--left {
padding-right: 4px;
margin-left: 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;
#entry-btns.minimized .entry-btn--right {
padding-left: 4px;
margin-right: calc(-4px - var(--width));
}
@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: 5.6vw;
font-size: 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;
@ -153,21 +123,20 @@ 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;
}
.page-cover .links {
[data-template="author"] .page-cover,
[data-template="category"] .page-cover,
[data-template="year"] .page-cover {
height: initial;
}
#entry-btns {
display: none;
}
}

View file

@ -1,9 +1 @@
.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;
}

View file

@ -8,11 +8,10 @@ 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;
@ -39,7 +38,7 @@ html {
* {
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255) transparent;
scrollbar-color: rgba(255, 255, 255, 0.25) transparent;
}
/* Works on Chrome, Edge, and Safari */
@ -54,7 +53,7 @@ html {
}
*::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255);
background-color: rgba(255, 255, 255, 0.25);
border-radius: 0px;
border: none;
}
@ -70,7 +69,6 @@ html {
}
main {
width: min(60vw, 45rem);
padding-left: var(--body-padding);
padding: 0 var(--body-padding);
}
}

View file

@ -1,3 +1,4 @@
/* ================= PANELS ================= */
#nav-overlay {
position: fixed;
inset: 0;
@ -104,9 +105,6 @@ button.search__icon {
padding: var(--unit--vertical) var(--unit--horizontal);
}
footer {
width: 100%;
}
.panel-close {
position: fixed;
box-sizing: border-box;
@ -136,15 +134,15 @@ footer {
}
/* ================= 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 p:not(:last-child) {
margin-bottom: var(--unit--vertical);
.panel-item-content__edito.short {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 5;
overflow: hidden;
}
button.see-more {
@ -156,37 +154,43 @@ button.see-more {
}
/* ================= TEXT ITEM ================= */
.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;
.text {
margin-bottom: var(--unit--vertical);
}
@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;

View file

@ -1,29 +0,0 @@
#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;
}

View file

@ -76,6 +76,18 @@ 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)));

View file

@ -28,12 +28,12 @@ h4 *,
h5,
h5 *,
p,
p *:not(strong) {
p * {
font-weight: var(--font-weight-light);
line-height: 1;
}
h1 {
h2 {
font-size: var(--font-size-xl);
}
@ -87,11 +87,11 @@ button,
}
.fs-l {
font-size: var(--font-size-l) !important;
line-height: calc(var(--unit--vertical) * 1.3);
line-height: var(--unit--vertical);
}
.fs-xl {
font-size: var(--font-size-xl) !important;
line-height: calc(var(--unit--vertical) * 2) !important;
line-height: calc(var(--unit--vertical) * 1.5) !important;
}
.fs-xxl {
font-size: var(--font-size-xxl) !important;
@ -137,14 +137,6 @@ button {
align-items: center;
}
a {
word-break: break-word;
}
a > * {
word-break: normal;
}
a * {
transition: font 0.2s ease-in-out;
}
@ -174,29 +166,14 @@ article p:not(:last-child) {
.footnote,
.footnote * {
font-style: normal !important;
scroll-margin-block-start: calc(var(--unit--vertical) * 6);
color: var(--color-primary);
color: var(--color-secondary);
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);
}
}

View file

@ -8,7 +8,6 @@
@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
View file

@ -1,181 +1,63 @@
"use strict";
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;
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");
}
}
// 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 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);
}
function setWindowHeightFactor() {
var windowHeight = window.innerHeight;
var min = 650;
var delta = windowHeight - min;
var factor = roundToNearestHalf(delta / 300) + 1;
document.querySelector(":root").style.setProperty("--window-height-factor", factor);
var head = document.querySelector("head");
var style = document.createElement("style");
style.innerText = ":root { --window-height-factor:".concat(factor, " }");
head.appendChild(style);
}
function roundToNearestHalf(num) {
var round = Math.round(num * 2) / 2;
return Math.max(round, 0);
}
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");
}
setWindowHeightFactor();
document.addEventListener("DOMContentLoaded", function () {
ragadjust("h1, h2, h4, h5", ["all"]);
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");
}
}
window.window.scrollTo({
top: 0
});
var handleScroll = throttle(function () {
window.addEventListener("scroll", 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

File diff suppressed because one or more lines are too long

View file

@ -9,36 +9,16 @@ function getUnit(id) {
return pxUnit;
}
// 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;
};
function throttle(callback, limit) {
let waiting = false;
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);
if (!waiting) {
callback.apply(this, arguments);
waiting = true;
setTimeout(function () {
waiting = false;
}, limit);
}
return result;
};
}
@ -67,25 +47,17 @@ 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("#", "");
} else if (href.includes("anc")) {
}
if (href.includes("anc")) {
footnote.id = footnote.hash.replace("anc", "sym").replace("#", "");
}
});
@ -104,30 +76,38 @@ 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 emailInput = document.querySelector("#subscribe-form input");
const email = document.querySelector("#subscribe-form input");
if (emailInput.value.toLowerCase().match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
if (email.value.toLowerCase().match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
const header = {
method: "POST",
body: JSON.stringify(emailInput.value),
body: email.value,
};
fetch("/subscribe.json", header)
.then((res) => res.json())
.then((data) => {
const formNode = emailInput.parentNode.parentNode;
formNode.outerHTML = "<p>" + data.message + "</p>";
});
fetch("/subscribe.json");
} else {
emailInput.value = "E-mail invalide. Recommencez.";
email.value = "E-mail invalide. Recommencez.";
}
}
const panelNav = document.querySelector(".panel");
const navOverlay = document.querySelector("#nav-overlay");
const openNavBtns = document.querySelectorAll("button.open-nav");
const openNavBtn = document.querySelector("button.open-nav");
const closeNavBtn = document.querySelector(".panel-close");
function closeNav() {
panelNav.classList.remove("panel--visible");
@ -136,18 +116,14 @@ function closeNav() {
}
document.addEventListener("DOMContentLoaded", () => {
ragadjust("h1, h2, h4, h5", ["all"]);
ragadjust("h1, h2, h3, h4, h5", ["all"]);
window.window.scrollTo({
top: 0,
});
const handleScroll = throttle(() => {
window.addEventListener("scroll", () => {
toggleLogoState();
if (window.innerWidth <= 680) {
toggleFooterState();
}
}, 100);
window.addEventListener("scroll", handleScroll);
});
setWindowHeightFactor();
window.addEventListener("resize", () => {
@ -193,12 +169,10 @@ document.addEventListener("DOMContentLoaded", () => {
});
});
openNavBtns.forEach((openNavBtn) => {
openNavBtn.addEventListener("click", () => {
panelNav.classList.add("panel--visible");
navOverlay.classList.add("nav-overlay--visible");
document.body.classList.add("no-scroll");
});
openNavBtn.addEventListener("click", () => {
panelNav.classList.add("panel--visible");
navOverlay.classList.add("nav-overlay--visible");
document.body.classList.add("no-scroll");
});
closeNavBtn.addEventListener("click", () => {
@ -207,4 +181,6 @@ document.addEventListener("DOMContentLoaded", () => {
navOverlay.addEventListener("click", () => {
closeNav();
});
subscribeBtn.addEventListener("click", showSubscribeField);
});

View file

@ -23,21 +23,11 @@
},
"require": {
"php": "~8.1.0 || ~8.2.0 || ~8.3.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"
"getkirby/cms": "^4.0"
},
"config": {
"platform": {
"php": "8.3.0"
},
"allow-plugins": {
"getkirby/composer-installer": true,
"php-http/discovery": true
"getkirby/composer-installer": true
},
"optimize-autoloader": true
},

2305
composer.lock generated

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 38 KiB

View file

@ -1,21 +0,0 @@
{
"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"
}

View file

@ -1,8 +0,0 @@
label: Corps
type: writer
headings:
- 3
marks:
- bold
- italic
- link

View file

@ -9,16 +9,11 @@ tabs:
type: fields
fields:
presentation:
extends: fields/body
label: Présentation
help: |
Optionnelle, sans mention du poste. Peut inclure un lien.
Exemple : "Co-fondateur des éditions [Athom](http://www.athom.xyz/)."
type: writer
help: Optionnelle
texts:
label: Textes
type: pages
create: false
query: page.getTexts()
seo:
extends: seo/page
label: Indexation

View file

@ -7,6 +7,3 @@ tabs:
label: Liste
type: pages
template: author
seo:
extends: seo/page
label: Indexation

View file

@ -6,41 +6,21 @@ image:
back: black
color: white
icon: email
options:
changeStatus: false
tabs:
contentTab:
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
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

View file

@ -18,17 +18,6 @@ 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
@ -42,9 +31,4 @@ tabs:
- image
- line
- quote
metaTab: tabs/meta
seo:
extends: seo/page
label: Indexation

View file

@ -15,7 +15,6 @@ tabs:
label: Corps
type: fields
fields:
body: fields/body
seo:
extends: seo/page
label: Indexation
body:
label: Corps
type: writer

View file

@ -15,36 +15,7 @@ tabs:
label: Corps
type: fields
fields:
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
type: writer
metaTab: tabs/meta
seo:
extends: seo/page
label: Indexation

View file

@ -3,26 +3,19 @@ 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: Lettres
label: Emails
type: pages
template: email
info: "{{ page.status == 'listed' ? 'envoyée' : 'brouillon' }}"
seo:
extends: seo/page
label: Indexation
info: "{{ page.month.toDate('M Y') }}"
sortBy: month desc

View file

@ -1,30 +1,24 @@
title: texts
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
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

View file

@ -4,30 +4,16 @@ image:
back: black
color: white
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
columns:
- width: 1/3
sections:
texts:
label: Textes
type: pages
templates:
- linear
- grid
- width: 2/3
fields:
edito:
type: writer

View file

@ -9,7 +9,6 @@ tabs:
type: writer
nodes: false
marks: false
edito: fields/body
seo:
extends: seo/site
label: Indexation
edito:
label: Éditorial
type: writer

View file

@ -3,9 +3,6 @@ sections:
metadata:
type: fields
fields:
keywords:
label: Mots-clés
type: tags
published:
label: Date de publication
type: date

View file

@ -1,13 +0,0 @@
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

View file

@ -0,0 +1,25 @@
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

View file

@ -1,28 +1,26 @@
<?php
function createEmptyCategories()
{
$categories = new Pages();
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);
}
}
return $categories;
}
function createCategories()
{
function createCategories() {
$emptyCategories = createEmptyCategories();
foreach (page('textes')->grandChildren() as $text) {
try {

View file

@ -1,29 +1,25 @@
<?php
return [
return array(
'debug' => true,
'panel' => [
'panel' => array(
'menu' => require __DIR__ . '/menu.php',
'css' => 'assets/css/panel.css',
],
'mailerSendApiKey' => 'mlsn.0a9f20751951e3c2d130b1d6c3749b0a0f5b14f1c52da65a3369d658c736513c',
'email' => [
'css' => 'assets/css/panel.css'
),
'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',
],
'type' => 'smtp',
'host' => 'smtp.outlook.com',
'port' => 587,
'security' => 'tls',
'auth' => true,
'username' => 'adrien.payet@outlook.com',
'password' => 't8nVpxCpEZcqH8y'
]
],
'routes' => [
'routes' => array(
require __DIR__ . '/routes/virtual-author.php',
require __DIR__ . '/routes/virtual-category.php',
require __DIR__ . '/routes/subscribe.php',
require __DIR__ . '/routes/virtual-pending.php',
],
'hooks' => [
'page.create:after' => require __DIR__ . '/hooks/prefill-test-adress-list.php',
],
];
require __DIR__ . '/routes/send-newsletter.php',
),
);

View file

@ -1,17 +0,0 @@
<?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),
]);
}
};

View file

@ -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');
},
}
],
'subscription' => [
'icon' => 'email',
'label' => 'Liste de diffusion',
'link' => 'pages/lettre',
'newsletter' => [
'icon' => 'email',
'label' => 'Liste de diffusion',
'link' => 'pages/liste-de-diffusion',
'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');
},
}
],
'-',
'-',

View file

@ -0,0 +1,24 @@
<?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()]);
}
}
];

View file

@ -1,43 +0,0 @@
<?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.',
];
}
},
];

View file

@ -0,0 +1,25 @@
<?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(),
]
]
);
}
];

View file

@ -5,33 +5,18 @@ use Kirby\Uuid\Uuid;
return [
'pattern' => 'categories/(:any)',
'action' => function ($category) {
$kirby = kirby();
$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),
return Page::factory(
[
'slug' => '',
'template' => 'category',
'model' => 'categories',
'content' => [
'title' => $category,
'uuid' => Uuid::generate(),
],
]);
},
]
]
);
}
];

View file

@ -1,18 +0,0 @@
<?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(),
],
]);
},
];

View file

@ -1,12 +0,0 @@
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

View file

@ -1,15 +0,0 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:vue/recommended",
"prettier"
],
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
}
}

View file

@ -1,2 +0,0 @@
.DS_Store
node_modules

View file

@ -1,21 +0,0 @@
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.

View file

@ -1,111 +0,0 @@
# Kirby Code editor
Code editor field for Kirby 3 and 4.
![screenshot-code-editor](https://user-images.githubusercontent.com/14079751/109679014-7b043800-7b7b-11eb-8c4e-2ae25da8288d.png)
<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)

View file

@ -1,20 +0,0 @@
{
"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"
}

View file

@ -1 +0,0 @@
.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}

File diff suppressed because one or more lines are too long

View file

@ -1,15 +0,0 @@
<?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',
],
]);

View file

@ -1,32 +0,0 @@
<?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;

File diff suppressed because it is too large Load diff

View file

@ -1,30 +0,0 @@
{
"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"
}
}

View file

@ -1,62 +0,0 @@
/* 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;

View file

@ -1,29 +0,0 @@
@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;
}

View file

@ -1,72 +0,0 @@
<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>

View file

@ -1,7 +0,0 @@
import CodeEditor from "./components/field/CodeEditor.vue";
window.panel.plugin("sylvainjule/code-editor", {
fields: {
"code-editor": CodeEditor,
},
});

View file

@ -1,38 +0,0 @@
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));

View file

@ -23,10 +23,15 @@ function setTitleFontSizeClass($title, $level = 'h1')
function getAuthorBySlug($slug)
{
$site = site();
$authors = page("auteurs")->children();
$author = $authors->find($slug);
$kirby = kirby();
$author = '';
foreach ($kirby->users() as $user) {
if (Str::slug($user->name()) === $slug) {
$author = $user;
}
}
return $author;
}

View file

@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm run build && git add index.css index.js

View file

@ -1 +0,0 @@
20

View file

@ -1,59 +0,0 @@
<?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);

View file

@ -1,21 +0,0 @@
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.

View file

@ -1,30 +0,0 @@
![Kirby SEO Banner](/.github/new-banner.png)
<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

View file

@ -1,33 +0,0 @@
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

View file

@ -1,48 +0,0 @@
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

View file

@ -1,31 +0,0 @@
<?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;
};

View file

@ -1,55 +0,0 @@
<?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,
];
};

View file

@ -1,49 +0,0 @@
<?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,
];
};

View file

@ -1,30 +0,0 @@
<?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
];
};

View file

@ -1,29 +0,0 @@
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

View file

@ -1,70 +0,0 @@
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

View file

@ -1,670 +0,0 @@
<?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;
}
}

View file

@ -1,29 +0,0 @@
<?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'] ?? [];
}
}

View file

@ -1,82 +0,0 @@
<?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();
}
}

View file

@ -1,100 +0,0 @@
<?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;
}
}

View file

@ -1,113 +0,0 @@
<?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);
}
}

View file

@ -1,41 +0,0 @@
{
"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
}
}

View file

@ -1,102 +0,0 @@
<?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,
];
}
]
]
];

View file

@ -1,11 +0,0 @@
<?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.');
}
];

View file

@ -1,27 +0,0 @@
<?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());
}
},
];

View file

@ -1,82 +0,0 @@
<?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
];

View file

@ -1,37 +0,0 @@
<?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()
);
}
}
}
};

View file

@ -1,11 +0,0 @@
<?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(),
];

View file

@ -1,98 +0,0 @@
<?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');
}
]
];

View file

@ -1,20 +0,0 @@
<?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']
]
];

View file

@ -1,28 +0,0 @@
<?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);
}
];

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,69 +0,0 @@
<?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}();
}
}

View file

@ -1,17 +0,0 @@
{
"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"
}
}

View file

@ -1,6 +0,0 @@
export default {
plugins: {
'postcss-logical': {},
autoprefixer: {}
}
}

View file

@ -1,13 +0,0 @@
<?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;
}

View file

@ -1,58 +0,0 @@
<?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}";
}
}

View file

@ -1,8 +0,0 @@
<?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