Nieuwe reviews Loes en Verble, geplande publicatie op vrijdag, tabel-wrap

- Reviews Loes (AI-chatassistent) en Verble (AI-speechwriter) toegevoegd,
  incl. screenshots en logo's
- new-review.mjs: publicatiedatum automatisch op eerstvolgende vrijdag na
  de laatste review (wekelijkse cadans)
- publish.ts: geplande reviews blijven zichtbaar in dev-mode, ongeacht datum
- rehype-wrap-tables.mjs: brede tabellen (prijsvergelijkingen) scrollen nu
  horizontaal i.p.v. de pagina te verbreken
- Fiskr-publicatiedatum bijgewerkt
This commit is contained in:
2026-07-27 22:00:47 +02:00
parent 640a862d1c
commit a7b752eccd
15 changed files with 439 additions and 12 deletions

View File

@@ -11,6 +11,9 @@ DEPLOY_PATH := /var/www/aitestbank/dist/
# Zorgt zelf dat Node 22 (uit .nvmrc) actief is via nvm, ongeacht welke node # Zorgt zelf dat Node 22 (uit .nvmrc) actief is via nvm, ongeacht welke node
# er in de huidige shell actief staat — anders faalt --env-file-if-exists # er in de huidige shell actief staat — anders faalt --env-file-if-exists
# stilletjes op een oudere systeem-node (zoals het standaard nvm-alias 20). # stilletjes op een oudere systeem-node (zoals het standaard nvm-alias 20).
#
# Publicatiedatum wordt automatisch gezet op: laatste reviewdatum + 1 week,
# afgerond naar vrijdag (wekelijkse cadans). Geen eerdere review? → eerstvolgende vrijdag.
review: review:
@if [ -z "$(URL)" ]; then \ @if [ -z "$(URL)" ]; then \
echo "Gebruik: make review URL=https://voorbeeld.nl [SLUG=voorbeeld]"; \ echo "Gebruik: make review URL=https://voorbeeld.nl [SLUG=voorbeeld]"; \

View File

@@ -1,11 +1,13 @@
import { defineConfig } from "astro/config"; import { defineConfig } from "astro/config";
import remarkEmoji from "remark-emoji"; import remarkEmoji from "remark-emoji";
import remarkFlagEmoji from "./src/lib/remark-flag-emoji.mjs"; import remarkFlagEmoji from "./src/lib/remark-flag-emoji.mjs";
import rehypeWrapTables from "./src/lib/rehype-wrap-tables.mjs";
export default defineConfig({ export default defineConfig({
site: "https://aitestbank.nl", site: "https://aitestbank.nl",
output: "static", output: "static",
markdown: { markdown: {
remarkPlugins: [remarkEmoji, remarkFlagEmoji], remarkPlugins: [remarkEmoji, remarkFlagEmoji],
rehypePlugins: [rehypeWrapTables],
}, },
}); });

19
public/logos/loes.svg Normal file
View File

@@ -0,0 +1,19 @@
<svg width="172" height="49" viewBox="0 0 172 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_39_1972)">
<path d="M28.9952 34.0298L24.4996 30.939L23.7354 31.4317C16.2278 36.2247 15.0589 41.3313 14.6094 46.7066C15.4635 47.0649 16.4076 47.2889 17.3517 47.5577C17.7113 42.5407 18.4756 38.4196 24.4546 34.2985L26.5676 35.732L28.4557 42.9886C28.8603 44.6012 29.5346 46.169 30.0741 47.7368C30.9732 47.5129 31.8723 47.2441 32.7265 46.9306C32.187 45.3627 31.5576 43.8845 31.153 42.2719L28.9952 34.0298Z" fill="white"/>
<path d="M18.7914 27.8034L17.9822 27.4002C9.98014 23.2791 4.94511 24.8917 0 27.221C0.134867 28.2065 0.224778 29.1472 0.449556 30.0879C5.12493 27.8034 9.12598 26.2356 15.8693 29.416L15.6895 31.9244L10.3398 37.2102C9.03607 38.5092 7.91218 39.9426 6.69838 41.3313C7.37271 42.0032 8.092 42.5407 8.81129 43.123C9.98014 41.7792 11.0591 40.3906 12.2729 39.1811L18.3419 33.1787L18.7465 27.7586L18.7914 27.8034Z" fill="white"/>
<path d="M13.7583 23.6375L18.6584 21.3082V20.3675C18.2089 11.4534 14.2977 7.91464 9.84712 4.82383C9.03792 5.40616 8.31863 6.03328 7.59934 6.6604C11.8701 9.52724 15.1968 12.2149 15.7813 19.5612L13.4885 20.6362L6.25068 18.6653C4.49741 18.1726 2.65423 17.9486 0.811052 17.5902C0.541319 18.4861 0.271585 19.382 0.136719 20.2779C1.93494 20.5915 3.73316 20.8602 5.48643 21.353L13.7583 23.5927V23.6375Z" fill="white"/>
<path d="M19.7802 14.9026L24.2758 17.9934L25.04 17.5006C32.3678 12.7972 33.6715 7.82506 34.1211 2.58412C33.2669 2.18097 32.3228 1.86741 31.3788 1.59864C31.0191 6.52602 30.2099 10.6023 24.2758 14.6786L22.1629 13.2452L20.2748 5.98849C19.8252 4.2863 19.1509 2.62891 18.5664 0.971519C17.6673 1.19549 16.7682 1.50905 15.9141 1.77782C16.4985 3.39042 17.1279 5.00301 17.5774 6.7052L19.7353 14.9474L19.7802 14.9026Z" fill="white"/>
<path d="M32.8615 19.4716L33.0414 16.9631L38.3911 11.6774C39.5599 10.5127 40.5489 9.21368 41.6279 8.00424C40.9985 7.33232 40.2792 6.7052 39.6049 6.12287C38.5709 7.33232 37.5819 8.58656 36.458 9.70642L30.389 15.7089L29.9844 21.129L30.7936 21.5321C34.39 23.3687 37.3571 24.0854 40.0095 24.0854C42.9765 24.0854 45.539 23.1895 48.0116 22.0697C47.9216 21.0842 47.8317 20.0987 47.6519 19.158C43.2912 21.2634 39.3351 22.5176 32.9514 19.4716H32.8615Z" fill="white"/>
<path d="M34.9744 25.2501L30.0742 27.5794V28.5201C30.5238 37.1206 34.1652 40.7489 38.436 43.7501C39.2002 43.1678 39.9644 42.5855 40.6837 41.9136C36.6377 39.1811 33.5358 36.4039 32.9963 29.3264L35.2891 28.2513L42.5269 30.2222C44.0554 30.6254 45.6288 30.8494 47.2023 31.1629C47.472 30.267 47.6968 29.3712 47.8766 28.4753C46.3481 28.1617 44.7747 27.9825 43.2912 27.5794L35.0193 25.3397L34.9744 25.2501Z" fill="white"/>
</g>
<path d="M161.525 39.9733C161.525 38.7583 161.134 37.7492 160.351 36.946C159.589 36.1222 158.601 35.4117 157.385 34.8145C156.191 34.1967 154.914 33.5994 153.555 33.0228C152.196 32.4461 150.909 31.8077 149.694 31.1075C148.499 30.3867 147.511 29.5218 146.728 28.5127C145.966 27.4829 145.585 26.2164 145.585 24.713C145.585 23.0243 146.017 21.593 146.882 20.4191C147.768 19.2247 148.89 18.2567 150.25 17.5153C151.609 16.7534 153.04 16.1973 154.543 15.8472C156.067 15.4971 157.468 15.3221 158.745 15.3221C160.763 15.3221 162.534 15.6207 164.058 16.2179C165.603 16.8151 166.797 17.6698 167.641 18.7819C168.506 19.894 168.939 21.2326 168.939 22.7978C168.939 24.0334 168.63 25.0013 168.012 25.7015C167.394 26.4017 166.591 26.7519 165.603 26.7519C164.614 26.7519 163.821 26.5047 163.224 26.0105C162.627 25.4956 162.328 24.8572 162.328 24.0952C162.328 23.292 162.483 22.7257 162.791 22.3962C163.1 22.0461 163.409 21.768 163.718 21.5621C164.027 21.3562 164.182 21.0473 164.182 20.6354C164.182 19.6674 163.595 19.0084 162.421 18.6583C161.267 18.2876 160.042 18.1023 158.745 18.1023C157.447 18.1023 156.201 18.2773 155.007 18.6274C153.812 18.9775 152.824 19.5233 152.041 20.2647C151.279 21.0061 150.898 21.9534 150.898 23.1067C150.898 24.1364 151.279 25.0013 152.041 25.7015C152.803 26.3812 153.781 26.999 154.976 27.555C156.17 28.1111 157.437 28.6877 158.776 29.2849C160.114 29.8616 161.381 30.5515 162.575 31.3547C163.77 32.1578 164.748 33.1566 165.51 34.3511C166.272 35.525 166.653 37.0078 166.653 38.7995C166.653 40.4058 166.262 41.8371 165.479 43.0934C164.696 44.3496 163.626 45.4102 162.266 46.2752C160.907 47.1401 159.342 47.7991 157.571 48.2522C155.82 48.7053 153.967 48.9318 152.01 48.9318C149.766 48.9318 147.768 48.5714 146.017 47.8506C144.288 47.1092 142.928 46.1001 141.94 44.8233C140.951 43.5258 140.457 42.0534 140.457 40.4058C140.457 39.0672 140.848 37.9448 141.631 37.0387C142.413 36.1119 143.34 35.6485 144.411 35.6485C145.338 35.6485 146.1 35.9266 146.697 36.4826C147.315 37.0387 147.624 37.8109 147.624 38.7995C147.624 39.685 147.397 40.3028 146.944 40.6529C146.491 40.9824 146.038 41.2399 145.585 41.4252C145.132 41.6106 144.905 41.9092 144.905 42.3211C144.905 43.1037 145.255 43.7833 145.956 44.3599C146.656 44.916 147.541 45.3484 148.612 45.6573C149.704 45.9457 150.837 46.0898 152.01 46.0898C155.1 46.0898 157.458 45.4617 159.085 44.2054C160.711 42.9492 161.525 41.5385 161.525 39.9733Z" fill="white"/>
<path d="M110.137 37.6875C110.137 34.5983 110.703 31.7151 111.836 29.0379C112.989 26.34 114.513 23.9717 116.408 21.9329C118.323 19.8941 120.434 18.3083 122.74 17.1756C125.068 16.0223 127.395 15.4457 129.722 15.4457C133.182 15.4457 135.725 16.0326 137.352 17.2065C138.979 18.3598 139.792 20.203 139.792 22.7361C139.792 24.6719 139.308 26.3297 138.341 27.7096C137.393 29.0688 136.127 30.2015 134.541 31.1076C132.955 31.9932 131.205 32.7037 129.289 33.2391C127.374 33.7746 125.459 34.1761 123.544 34.4439C121.628 34.7116 119.867 34.8969 118.261 34.9999C117.54 35.0411 117.056 35.2573 116.809 35.6486C116.562 36.0193 116.439 36.6989 116.439 37.6875C116.439 39.9734 116.881 41.899 117.767 43.4641C118.673 45.0087 120.187 45.781 122.308 45.781C123.935 45.781 125.418 45.3897 126.756 44.6071C128.115 43.8039 129.32 42.8154 130.371 41.6415C131.441 40.4471 132.348 39.2526 133.089 38.0582C133.254 37.7904 133.418 37.636 133.583 37.5948C133.769 37.533 134.088 37.5021 134.541 37.5021H135.437C136.034 37.5021 136.384 37.5639 136.487 37.6875C136.59 37.7904 136.508 38.0787 136.24 38.5524C135.354 40.2411 134.201 41.8681 132.78 43.4332C131.38 44.9984 129.742 46.2855 127.868 47.2947C125.994 48.3038 123.894 48.8083 121.567 48.8083C119.281 48.8083 117.283 48.4376 115.574 47.6962C113.864 46.9549 112.526 45.7707 111.558 44.1437C110.61 42.5168 110.137 40.3647 110.137 37.6875ZM118.755 32.1888C120.321 32.0035 121.978 31.7151 123.729 31.3238C125.479 30.9326 127.117 30.3868 128.641 29.6866C130.185 28.9864 131.441 28.0803 132.409 26.9682C133.377 25.8355 133.861 24.4248 133.861 22.7361C133.861 20.9649 133.511 19.7911 132.811 19.2144C132.111 18.6378 131.081 18.3495 129.722 18.3495C128.074 18.3495 126.458 18.8643 124.872 19.8941C123.307 20.9238 121.896 22.3242 120.64 24.0953C119.404 25.8458 118.416 27.8434 117.674 30.0882C117.406 30.8914 117.314 31.468 117.396 31.8181C117.499 32.1682 117.952 32.2918 118.755 32.1888Z" fill="white"/>
<path d="M76.2305 37.0696C76.2305 34.1247 76.7144 31.3547 77.6824 28.7599C78.6709 26.1444 80.0198 23.8481 81.7291 21.8711C83.4384 19.8735 85.4052 18.3083 87.6294 17.1756C89.8535 16.0223 92.2116 15.4457 94.7035 15.4457C97.1954 15.4457 99.3578 15.9503 101.191 16.9594C103.044 17.9685 104.475 19.3586 105.485 21.1297C106.494 22.9008 106.998 24.919 106.998 27.1844C106.998 30.1088 106.504 32.8787 105.515 35.4942C104.548 38.1096 103.209 40.4162 101.5 42.4138C99.7903 44.3909 97.8235 45.956 95.5993 47.1093C93.3752 48.242 91.0171 48.8083 88.5252 48.8083C86.0333 48.8083 83.8606 48.3038 82.0071 47.2947C80.1743 46.2855 78.7533 44.8954 77.7441 43.1243C76.735 41.3532 76.2305 39.335 76.2305 37.0696ZM82.7176 37.0696C82.7176 38.5936 82.903 40.0455 83.2737 41.4253C83.665 42.7845 84.2828 43.8966 85.1272 44.7616C85.9715 45.6059 87.1042 46.0281 88.5252 46.0281C90.2139 46.0281 91.7791 45.503 93.2207 44.4527C94.6829 43.4023 95.9597 41.9814 97.0512 40.1897C98.1427 38.3774 98.9871 36.3488 99.5843 34.1041C100.202 31.8593 100.511 29.5527 100.511 27.1844C100.511 25.6604 100.326 24.2188 99.955 22.8596C99.5843 21.4798 98.9665 20.3677 98.1015 19.5234C97.2572 18.6584 96.1245 18.2259 94.7035 18.2259C93.0148 18.2259 91.4393 18.7511 89.9771 19.8014C88.5355 20.8517 87.269 22.283 86.1775 24.0953C85.086 25.887 84.2313 27.9052 83.6135 30.15C83.0163 32.3741 82.7176 34.6807 82.7176 37.0696Z" fill="white"/>
<path d="M72.3193 2.03883L63.33 42.6918C63.2888 42.8566 63.2579 43.0419 63.2373 43.2479C63.2167 43.4538 63.2064 43.6391 63.2064 43.8039C63.2064 44.5865 63.5771 44.9778 64.3185 44.9778C65.2864 44.9778 66.3573 44.3188 67.5312 43.0007C68.7257 41.6621 69.7039 40.0763 70.4659 38.2435C70.6306 37.8316 70.7748 37.5947 70.8983 37.533C71.0219 37.4506 71.3102 37.4918 71.7633 37.6565L72.3811 37.8728C72.7724 38.0169 72.9784 38.1508 72.999 38.2743C73.0401 38.3979 72.9784 38.6347 72.8136 38.9849C71.6191 41.8474 70.0643 44.2055 68.149 46.059C66.2338 47.8918 64.2361 48.8083 62.1561 48.8083C60.7145 48.8083 59.623 48.4479 58.8816 47.7271C58.1402 46.9857 57.7695 45.8427 57.7695 44.2982C57.7695 44.1334 57.7798 43.9172 57.8004 43.6494C57.821 43.3611 57.8519 43.1243 57.8931 42.9389L66.1102 4.51014C66.2338 3.95409 66.2029 3.63488 66.0175 3.5525C65.8322 3.44953 65.41 3.39805 64.751 3.39805H61.909C61.5589 3.39805 61.322 3.33626 61.1985 3.2127C61.0955 3.08913 61.044 2.8626 61.044 2.53309V1.17387C61.044 0.679609 61.1058 0.360397 61.2294 0.216238C61.3735 0.0720794 61.6824 0 62.1561 0H70.435C71.5883 0 72.237 0.113269 72.3811 0.339806C72.5459 0.566341 72.5253 1.13268 72.3193 2.03883Z" fill="white"/>
<defs>
<clipPath id="clip0_39_1972">
<rect width="172" height="46.8101" fill="white" transform="translate(0 0.926723)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
public/logos/scribbr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

11
public/logos/verble.svg Normal file
View File

@@ -0,0 +1,11 @@
<svg width="145" height="30" viewBox="0 0 145 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M58.0532 10.6678L62.3879 10.7076C62.9863 10.7076 63.3054 11.3976 62.9464 11.862C62.5608 12.3529 62.1486 12.8704 61.9757 13.1358C61.5901 13.693 61.2178 14.3963 60.8322 15.2322L54.9418 28.4078H52.5883L45.9001 13.64C45.4613 12.6581 44.7699 11.7824 43.8125 11.0128V10.7342H54.4897V11.0128C53.5191 11.7028 53.0271 12.406 53.0271 13.1225C53.0271 13.6267 53.1999 14.2636 53.5457 15.0066C53.8648 15.7231 54.2238 16.5325 54.6094 17.4215C55.0083 18.337 55.4737 19.3587 55.9789 20.4998C56.4177 21.4551 56.6438 21.9726 56.6571 22.0256C58.3191 18.2176 59.2898 15.8823 59.5823 15.0199C59.7419 14.5555 59.835 14.1309 59.835 13.7461C59.835 13.2817 59.702 12.8571 59.4494 12.4458C59.1701 12.0345 58.7181 11.5303 58.0665 10.9597V10.6678H58.0532Z" fill="#07284E"></path>
<path d="M79.7268 21.5745L79.8996 21.6939C78.9024 26.3246 76.1766 28.6333 71.7222 28.6333C69.0363 28.6333 66.8024 27.8107 65.0074 26.1521C63.2256 24.5068 62.3347 22.2114 62.3347 19.2791C62.3347 16.36 63.2256 14.1176 65.0074 12.5254C66.8024 10.9332 69.0496 10.1504 71.7754 10.1504C74.4347 10.1504 76.4425 10.8669 77.8121 12.2866C79.1949 13.7196 79.873 15.3516 79.873 17.1694V17.5807H68.4911V17.8991C68.4911 20.115 69.1027 21.7337 70.3127 22.7687C71.5227 23.8036 72.9854 24.3078 74.7139 24.3078C76.9877 24.3211 78.6497 23.4056 79.7268 21.5745ZM71.1238 11.1853H71.1371C69.5415 11.1853 68.6773 12.9898 68.5177 16.5856H73.8364C73.7699 12.9898 72.8524 11.1853 71.1238 11.1853Z" fill="#07284E"></path>
<path d="M80.1789 28.3149L80.1656 28.0893C80.9767 27.5984 81.4819 27.1473 81.6947 26.7094C81.9074 26.2715 82.0138 25.3958 82.0138 24.0955V16.3865C82.0138 15.0332 81.9074 14.1044 81.6814 13.5736C81.4553 13.0562 80.9634 12.4989 80.1921 11.9018V11.6497C82.5855 11.5303 84.9922 11.1588 87.4122 10.5219C88.0638 10.3494 88.6887 10.8536 88.6621 11.517L88.6089 13.7328H88.702C89.0211 12.8041 89.6062 11.9947 90.4705 11.2782C91.3215 10.5617 92.4517 10.2167 93.8744 10.2167C94.4728 10.2167 94.9913 10.3228 95.4434 10.5484V17.2357C95.4301 17.3286 95.3503 17.3949 95.2706 17.3949C95.2174 17.3949 95.1642 17.3684 95.1243 17.3153C93.4223 15.0332 91.1619 14.6218 90.1247 14.6218C89.5663 14.6218 89.1408 14.927 88.8483 15.5241V24.0557C88.8483 25.3428 89.0078 26.2185 89.3137 26.6696C89.6062 27.1075 90.2976 27.5719 91.3746 28.0495V28.3016H80.1789V28.3149Z" fill="#07284E"></path>
<path d="M109.631 10.1238L109.618 10.1371C110.482 10.1371 111.32 10.3096 112.131 10.6678C112.942 11.0261 113.686 11.5435 114.378 12.2335C115.096 12.95 115.654 13.8921 116.067 15.073C116.492 16.2804 116.705 17.6603 116.705 19.1995C116.705 22.1052 115.973 24.4007 114.511 26.0858C113.048 27.7709 111.133 28.6068 108.74 28.6068C107.583 28.6068 106.546 28.3812 105.642 27.9168C104.764 27.4657 103.993 27.0146 103.342 26.5502C102.717 26.0991 102.171 25.8735 101.719 25.8735C100.775 25.8735 99.5122 26.8155 97.89 28.7129H97.3714V5.67888C97.3714 5.14815 97.265 4.72356 97.0523 4.40511C96.813 4.04686 96.3077 3.48959 95.5365 2.72002V2.44138C97.8767 2.28216 100.217 1.91064 102.531 1.3401C103.381 1.1278 104.193 1.76469 104.193 2.64041V13.1225H104.232C105.642 11.119 107.45 10.1238 109.631 10.1238ZM107.384 27.2136C108.873 27.2136 109.604 24.9447 109.604 20.4069C109.604 15.2322 108.647 12.6316 106.719 12.6316C105.801 12.6316 104.95 13.2419 104.153 14.4626V21.0172C104.153 22.0787 104.326 23.1269 104.685 24.122C104.897 24.7191 105.15 25.2631 105.442 25.7673C105.988 26.7359 106.639 27.2136 107.384 27.2136Z" fill="#07284E"></path>
<path d="M116.824 28.3149L116.838 27.9832C117.649 27.4922 118.154 27.0411 118.367 26.6033C118.58 26.1654 118.686 25.2897 118.686 23.9894V7.90799C118.686 6.75363 118.619 5.89118 118.473 5.2941C118.327 4.69702 118.167 4.29897 117.995 4.08667C117.782 3.8213 117.396 3.46305 116.851 2.98539V2.72002C119.324 2.54753 121.638 2.17601 123.805 1.632C124.643 1.41971 125.467 2.05659 125.467 2.93231V24.0159C125.467 25.3029 125.587 26.1787 125.826 26.6298C126.066 27.0677 126.558 27.5453 127.315 28.0628V28.3149H116.824Z" fill="#07284E"></path>
<path d="M144.827 21.5745L145 21.6939C144.003 26.3246 141.277 28.6333 136.823 28.6333C134.137 28.6333 131.903 27.8107 130.108 26.1521C128.326 24.5068 127.435 22.2114 127.435 19.2791C127.435 16.36 128.326 14.1176 130.108 12.5254C131.903 10.9332 134.15 10.1504 136.876 10.1504C139.535 10.1504 141.543 10.8669 142.912 12.2866C144.295 13.7196 144.973 15.3516 144.973 17.1694V17.5807H133.591V17.8991C133.591 20.115 134.203 21.7337 135.413 22.7687C136.623 23.8036 138.086 24.3078 139.814 24.3078C142.088 24.3211 143.763 23.4056 144.827 21.5745ZM136.224 11.1853H136.238C134.642 11.1853 133.778 12.9898 133.618 16.5856H138.937C138.87 12.9898 137.966 11.1853 136.224 11.1853Z" fill="#07284E"></path>
<path d="M17.1394 19.7435L6.68822 2.06986C6.14305 0.862426 4.91976 0.0132446 3.49702 0.0132446C1.56901 0.0132446 0 1.57892 0 3.50285C0 4.13974 0.172856 4.73682 0.465383 5.24102L14.1343 28.4078C15.1848 30.0663 18.9743 30.9022 20.53 28.4741C21.0486 27.6647 23.4553 23.5382 23.4553 23.5382C21.514 24.5466 19.3599 23.4852 17.1394 19.7435Z" fill="#93C6FF"></path>
<path d="M25.3699 9.93808L20.7426 2.12295C20.2108 0.875719 18.9742 0 17.5249 0C15.5968 0 14.0278 1.56568 14.0278 3.48961C14.0278 4.28571 14.2938 5.01548 14.7459 5.59929L21.0884 16.3069C21.6069 17.3684 22.8568 18.1115 24.3327 18.1115C25.8486 18.1115 27.0984 17.4082 27.8696 16.1212C28.6276 14.8341 30.2365 11.9549 30.2365 11.9549C28.2685 12.9633 26.2475 11.3313 25.3699 9.93808Z" fill="#0092FF"></path>
<path d="M31.5664 0.0132446C29.6383 0.0132446 28.0693 1.57892 28.0693 3.50285C28.0693 5.42678 29.6383 6.99246 31.5664 6.99246C33.4944 6.99246 35.0634 5.42678 35.0634 3.50285C35.0634 1.57892 33.4944 0.0132446 31.5664 0.0132446Z" fill="#006BFF"></path>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -29,7 +29,7 @@
*/ */
import { chromium } from "playwright"; import { chromium } from "playwright";
import Anthropic from "@anthropic-ai/sdk"; import Anthropic from "@anthropic-ai/sdk";
import { mkdir, readFile, writeFile, access } from "node:fs/promises"; import { mkdir, readFile, writeFile, access, readdir } from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
@@ -45,6 +45,54 @@ function slugify(hostname) {
.replace(/(^-|-$)/g, ""); .replace(/(^-|-$)/g, "");
} }
/** YYYY-MM-DD in lokale tijd (middaguur om DST-randen te vermijden). */
function formatYmd(d) {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
}
function parseYmd(s) {
const [y, m, d] = s.split("-").map(Number);
return new Date(y, m - 1, d, 12, 0, 0, 0);
}
/** Schuif door tot de eerstvolgende vrijdag (blijft staan als 't al vrijdag is). */
function toFriday(d) {
const out = new Date(d);
const day = out.getDay(); // 0 zo … 5 vr
if (day !== 5) out.setDate(out.getDate() + ((5 - day + 7) % 7));
return out;
}
/**
* Publicatiedatum voor een nieuw concept: laatste reviewdatum + 1 week,
* afgerond naar vrijdag (wekelijkse cadans). Geen eerdere datum? → eerstvolgende vrijdag.
*/
async function nextPublishDate() {
const dir = path.join(projectRoot, "src", "content", "reviews");
const files = await readdir(dir);
let latest = null;
for (const file of files) {
if (!file.endsWith(".md") || file.startsWith("_")) continue;
const text = await readFile(path.join(dir, file), "utf-8");
// Alleen een echte frontmatter-date: "date: 2026-07-31" (geen commentaarregel)
const match = text.match(/^---\r?\n[\s\S]*?^date:\s*(\d{4}-\d{2}-\d{2})\b/m);
if (!match) continue;
if (!latest || match[1] > latest) latest = match[1];
}
if (latest) {
const d = parseYmd(latest);
d.setDate(d.getDate() + 7);
return formatYmd(toFriday(d));
}
return formatYmd(toFriday(new Date()));
}
function fillField(template, field, value) { function fillField(template, field, value) {
// Laat de regel (met het voorbeeld in de comment) ongewijzigd als er niets gevonden is, // Laat de regel (met het voorbeeld in de comment) ongewijzigd als er niets gevonden is,
// zodat de gebruiker nog steeds ziet wat er verwacht wordt en het zelf kan invullen. // zodat de gebruiker nog steeds ziet wat er verwacht wordt en het zelf kan invullen.
@@ -222,17 +270,17 @@ async function main() {
// instructie-comment bovenaan het template hoort niet in een echt reviewbestand // instructie-comment bovenaan het template hoort niet in een echt reviewbestand
content = content.replace(/# Kopieer dit bestand[\s\S]*?koppeltekens\.\n\n/, ""); content = content.replace(/# Kopieer dit bestand[\s\S]*?koppeltekens\.\n\n/, "");
// date/score horen pas een echte waarde te krijgen bij het publiceren. Het // Publicatiedatum: laatste bestaande review + 1 week, op vrijdag (cadans).
// template zet ze op respectievelijk "" en 0 als leesbaar voorbeeld voor de // draft blijft true tot je zelf klaar bent; de datum staat al klaar voor planning.
// handmatige workflow, maar allebei zijn dat *ongeldige* waarden voor het const publishDate = await nextPublishDate();
// Zod-schema (date: "" faalt de date-coercion, score: 0 faalt min(1)) — dus
// hier uitcommentariëren i.p.v. laten staan, zodat het veld echt afwezig is
// (de .optional() in het schema kan er dan mee uit de voeten) maar er wél
// zichtbaar blijft staan HOE je 'm moet invullen zodra je gaat publiceren.
content = content.replace( content = content.replace(
/^date: ""(\s*#.*)?$/m, /^date: ""(\s*#.*)?$/m,
"# date: 2026-01-01 # invullen zodra je draft: false zet", `date: ${publishDate} # auto: laatste review + 1 week (vrijdag)`,
); );
console.error(`Publicatiedatum: ${publishDate} (laatste review + 1 week, vrijdag)`);
// score: 0 is ongeldig voor het Zod-schema (min(1)) — uitcommentariëren zodat
// het veld afwezig is (optional zolang draft) maar de invulinstructie blijft.
content = content.replace( content = content.replace(
/^score: 0(\s*#.*)?$/m, /^score: 0(\s*#.*)?$/m,
"# score: 3 # geheel getal 1-5 (aantal robots) — invullen vóórdat je draft: false zet", "# score: 3 # geheel getal 1-5 (aantal robots) — invullen vóórdat je draft: false zet",

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 KiB

View File

@@ -4,7 +4,7 @@ company: "Fiskr"
website: "https://fiskr.nl/" website: "https://fiskr.nl/"
location: "Den Haag" location: "Den Haag"
category: "Boekhouding" category: "Boekhouding"
date: 2026-07-31 date: 2026-08-07
draft: false draft: false
# --- Specs-balkje bovenaan de review --- # --- Specs-balkje bovenaan de review ---

130
src/content/reviews/loes.md Normal file
View File

@@ -0,0 +1,130 @@
---
title: "Loes: Nederlandse AI-chatassistent, gratis en volledig Europees" # AI-concept, controleren!
company: "Loes"
website: "https://loes.ai/"
location: "Groningen"
category: "AI"
date: 2026-07-31
draft: false
# --- Specs-balkje bovenaan de review ---
for_who: "Iedereen die een Nederlandse AI-chatassistent wil gebruiken, voor ontwikkelaars ook beschikbaar via API"
price_from: "Gratis"
hosting: ":flag_eu:"
# --- Eindoordeel ---
score: 3
verdict: "Nog net niet zo ver"
# --- Cover ---
logo: "/logos/loes.svg"
---
## Wat is het
De Europese Unie loopt mijlenver achter op de Verenigde Staten en China als 't om
AI gaat. De huidige AVG (GDPR)-wetgeving maakt 't ook niet makkelijker om iets binnen
de grenzen van de EU te ontwikkelen. Dat weerhoudt `HostYourAI B.V.` echter niet
om wel een eigen open Nederlandse LLM aan te bieden middels een chatbot
(zoals we die kennen van ChatGPT) en een API (voor ontwikkelaars).
## Voor wie
Loes is bedoeld voor iedereen die de Nederlandse taal gebruikt. De chat is
gratis beschikbaar voor alle gebruikers. Voor ontwikkelaars biedt Loes ook een API
met een betaal-per-gebruikmodel.
## Wie zit erachter
Loes is een dienst van HostYourAI B.V. uit Groningen.
## Prijs
### Chat
De Loes-chat is gratis te gebruiken en biedt 50 berichten per dag aan. Wil je
meer dan dat (en snellere toegang), dan heb je `Chat Pro` nodig, wat €18 per maand
kost. Vergelijken we dat met de andere grote spelers, dan zien we:
| Dienst | Kosten per maand |
| ------ | ---------------: |
| Loes Pro | €18 |
| Mistral Pro | €14 |
| Claude Pro | $20 |
| ChatGPT Plus | $20 |
_(Let op: valutaverschil)_
### API
De API-versies alsook de prijzen veranderen erg snel in de AI-wereld. De genoemde
prijzen zijn dan ook de prijzen op het moment van schrijven.
| Provider | Model | Input / 1M | Output / 1M |
| -------- | ---- | ---------: | ----------: |
| Loes | Loes Large (~2227B) | €0,08 | €0,25 |
| Loes | Loes World 14B (1e gen) | €0,06 | €0,15 |
| Loes | 8B open (Llama/Qwen e.d.) | €0,05 | €0,12 |
| Mistral | Small 4 | $0,15 | $0,60 |
| Mistral | Large 3 | $0,50 | $1,50 |
| Mistral | Medium 3.5 | $1,50 | $7,50 |
| OpenAI | GPT-4.1 Nano (budget) | ~$0,10 | ~$0,40 |
| OpenAI | GPT-4.1 / mid flagship | ~$2,00 | ~$8,00 |
| OpenAI | GPT-5.x / Terra-achtig | ~$1,752,50 | ~$1415 |
| OpenAI | GPT-5.6 Sol e.d. | ~$5,00 | ~$30,00 |
| Claude | Haiku 4.5 | $1,00 | $5,00 |
| Claude | Sonnet 4.6 | $3,00 | $15,00 |
| Claude | Sonnet 5 | $2,00 | $10,00 | Daarna $3 / $15 |
| Claude | Opus 4.x / 5 | $5,00 | $25,00 |
Prijstechnisch is *Loes* dus uitermate competitief!
## Hands-on indruk
### De test
De API biedt wrappers aan zodat het een drop-in replacement is voor huidige tools.
Ik heb een aantal tests afgevuurd waarvoor ik normaliter Claude Sonnet gebruik. De
resultaten waren niet erg goed, maar dat is waarschijnlijk omdat `Loes Large EU` geen
tool-use ondersteunt, noch caching. De output die er kwam negeerde specifieke
instructies en was kwalitatief niet te vergelijken met die van Claude.
Na mijn nogal specifieke tests heb ik nog wat meer generieke tests gedaan met wisselend
resultaat:
| # | Test | Oordeel |
| --- | -------------------------------------- | ---------------------------------------- |
| 1 | Feitenkennis (hoofdstad Australië) | Goed, correct antwoord + correcte reden |
| 2 | Logica-puzzel (leeftijdsvolgorde) | Faalt volledig |
| 3 | Python-palindroomfunctie | Bug |
| 4 | Samenvatten in exact 2 zinnen | Faalt op 2 vlakken |
| 5 | Exact 3 bullets, geen intro/afsluiting | Goed |
| 6 | Nobelprijs 2031 (bestaat niet) | Goed, geen hallucinatie |
| 7 | Reken-woordprobleem (trein) | Interne inconsistentie |
| 8 | Nederlandse schrijfstijl (e-mail) | Grotendeels goed |
Bij de logicapuzzel werd het volgende gevraagd:
> Anna is ouder dan Bram. Bram is ouder dan Carla. Carla is jonger dan Anna maar ouder dan Daan. Wie is het jongst?
Er kwam geen antwoord, slechts het verzoek om meer data aan te leveren.
De palindroomfunctie vergat hoofdlettergebruik, zo zou "Level" onterecht niet
als palindroom gezien worden. De prompt was echter:
> Schrijf een Python-functie die controleert of een string een palindroom is, ongevoelig voor hoofdletters en spaties.
Bij de 4e test werd gevraagd om exact twee zinnen te gebruiken. De API negeerde dit en
gaf een paragraaf van 6 zinnen. Tevens traden hallucinaties op.
Rekenen ging Loes ook niet goed af. 2u45 werd geïnterpreteerd als 3,75 uur, wat
natuurlijk 2,75 uur moet zijn, waardoor het antwoord fout was.
## Conclusie
Ik sta te popelen om al mijn AI-abonnementen over te zetten naar een Europese speler,
helemaal als die uit Nederland komt. Helaas is *Loes* op dit moment nog geen
serieuze concurrent voor bijv. Claude. Voor dagelijks chatgebruik fungeert het
prima. Voor iets meer complexe zaken moeten we toch weer terug naar de concurrenten.
We blijven het op de voet volgen!

View File

@@ -0,0 +1,91 @@
---
title: "Verble: AI-assistent voor preken, redes en presentaties"
company: "Verble"
website: "https://www.verble.app/"
location: "Rotterdam"
category: "Schrijven"
date: 2026-08-14
draft: false
for_who: "Predikanten en andere sprekers"
price_from: "$8,00 /maand"
hosting: ":question:"
# --- Eindoordeel ---
score: 4
verdict: "Handig voor mensen die veel spreken"
# --- Cover ---
logo: "/logos/verble.svg"
---
## Wat is het
De meeste mensen die wel eens een speech hebben moeten schrijven lopen op
een gegeven moment vast. Wat is de boodschap die ik wil overbrengen, is het
niet te lang, niet te kort, niet te saai? Verble belooft hierbij te helpen.
## Voor wie
Verble richt zich op predikanten, gastheren, sprekers en leiders die toespraken,
preken, troostredes of toasts moeten voorbereiden. De tool is bedoeld voor
iedereen die in een belangrijk moment de juiste woorden nodig heeft: van
zondagse preken en bruiloftspreken tot presentaties op het werk.
## Wie zit erachter
Verble B.V. zegt een klein gedreven team te zijn uit Rotterdam.
## Prijs
De dienst hanteert verschillende prijzen voor de verschillende takken die ze
aanbieden.
| Tak | Basis | Pro | Enkele speech |
| --- | ----: | --: | ------------: |
| Preken | $12,50 | $19,00 | - |
| Bruiloft | $8,00 | - | $30,99 |
| Werk/leven | $12,50 | - | - |
_(Prijzen per maand, op basis van jaarlijkse facturering. Maandelijkse facturering
kan ook, maar is iets duurder)_
## Hands-on indruk
### De test
Voor de test heb ik gevraagd een rede te schrijven voor een team van software
engineers die door een moeilijke periode heen gegaan zijn, maar nu op volle
sterkte werken aan het herschrijven van oude software in een moderne tech stack,
klaar voor AI-agents.
Na het invoeren van de beginnende tekst stelt het de gebruiker vragen. Wat is het
doel? Wil je motiveren? Waar moet de focus op liggen? Zijn de nieuwe mensen die
je noemt aanwezig? De vragen zijn geen standaard vragen, maar hebben daadwerkelijk
betrekking op je input.
Als je klaar bent, kun je een draft genereren. Deze kun je vervolgens oefenen,
laten uitspreken of aanpassen met AI. Als je klaar bent, kun je de tekst
downloaden, uiteraard tegen betaling.
Ik vroeg om een motiverende, energieke tekst en die heb ik gekregen. Het is oprecht
motiverend en zit professioneel in elkaar.
## Conclusie
Als je veel presentaties of speeches geeft, lijkt me dit een uitstekende tool
om te gebruiken. Maar aangezien het een abonnementsvorm kent, lijkt het me
enkel nuttig als je het veel gebruikt. Voor een enkele speech lijkt me dat je
ook wel wegkomt met standaard ChatGPT, Claude of Grok. Een enkele trouwrede
kun je overigens ook eenmalig afrekenen, zonder abonnement.
De output is professioneel en erg aanpasbaar. Of het werkelijk veel toevoegt
ten opzichte van een chatbot als ChatGPT/Claude/Grok weet ik niet. Het werkt
wel erg snel, makkelijk en fijn.
De predikantversie heb ik niet getest, aangezien ik (als ongelovige) geen idee
heb hoe ik de output zou moeten beoordelen.
Jammer dat de taal (van de website) alleen in het Engels en Spaans weergegeven
kan worden.

View File

@@ -1,8 +1,14 @@
// Static site: "nu" is bevroren op het moment van de laatste build. Een // Static site: "nu" is bevroren op het moment van de laatste build. Een
// review met draft: false en een datum in de toekomst is dus pas écht // review met draft: false en een datum in de toekomst is dus pas écht
// zichtbaar zodra er ná die datum opnieuw gebouwd + gedeployed wordt. // zichtbaar zodra er ná die datum opnieuw gebouwd + gedeployed wordt.
//
// In `npm run dev` mag je een geplande review gewoon alvast zien (handig om
// 'm te proppen zonder de systeemklok te verzetten) — in een echte build
// (import.meta.env.DEV is dan false) geldt de datumcheck onverkort.
export function isPublished(data: { draft: boolean; date?: Date }): boolean { export function isPublished(data: { draft: boolean; date?: Date }): boolean {
return !data.draft && !!data.date && data.date.getTime() <= Date.now(); if (data.draft) return false;
if (import.meta.env.DEV) return true;
return !!data.date && data.date.getTime() <= Date.now();
} }
export function isScheduled(data: { draft: boolean; date?: Date }): boolean { export function isScheduled(data: { draft: boolean; date?: Date }): boolean {

View File

@@ -0,0 +1,27 @@
/**
* Wikkel elke <table> in <div class="table-wrap"> zodat brede markdown-tabellen
* horizontaal kunnen scrollen zonder de table-layout te breken.
*/
export default function rehypeWrapTables() {
return (tree) => {
walk(tree);
};
}
function walk(node) {
if (!node?.children?.length) return;
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
if (child.type === "element" && child.tagName === "table") {
node.children[i] = {
type: "element",
tagName: "div",
properties: { className: ["table-wrap"] },
children: [child],
};
continue;
}
walk(child);
}
}

View File

@@ -50,7 +50,7 @@ const dateFormatter = new Intl.DateTimeFormat("nl-NL", {
size="thumb" size="thumb"
/> />
<div class="meta"> <div class="meta">
<span class="tabnum">{live ? dateFormatter.format(post.data.date) : "—"}</span> <span class="tabnum">{live && post.data.date ? dateFormatter.format(post.data.date) : "—"}</span>
<span>·</span> <span>·</span>
<a href={`/categorieen#${categorySlug(post.data.category)}`}>{emojify(post.data.category)}</a> <a href={`/categorieen#${categorySlug(post.data.category)}`}>{emojify(post.data.category)}</a>
<span class={`status ${live ? "" : "pending"}`}>{live ? "gepubliceerd" : "binnenkort"}</span> <span class={`status ${live ? "" : "pending"}`}>{live ? "gepubliceerd" : "binnenkort"}</span>

View File

@@ -208,9 +208,99 @@ const dateFormatter = new Intl.DateTimeFormat("nl-NL", {
} }
.body :global(blockquote p) { margin: 14px 0; } .body :global(blockquote p) { margin: 14px 0; }
/* Markdown-tabellen: zelfde breedte als lopende tekst (64ch) */
.body :global(.table-wrap) {
max-width: 64ch;
margin: 0 0 24px;
border: 1px solid var(--line);
border-radius: var(--radius);
overflow-x: auto;
-webkit-overflow-scrolling: touch;
background: var(--paper);
}
.body :global(.table-wrap table) {
width: 100%;
margin: 0;
border-collapse: separate;
border-spacing: 0;
font-size: 15px;
line-height: 1.45;
border: none;
background: transparent;
}
.body :global(caption) {
caption-side: bottom;
padding: 10px 14px 0;
font-family: var(--mono);
font-size: 12.5px;
color: var(--muted);
text-align: left;
}
.body :global(thead) {
background: var(--paper-raised);
}
.body :global(th),
.body :global(td) {
padding: 11px 14px;
text-align: left;
vertical-align: top;
border-bottom: 1px solid var(--line);
}
.body :global(th) {
font-family: var(--mono);
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted);
white-space: nowrap;
}
.body :global(th:not(:last-child)),
.body :global(td:not(:last-child)) {
border-right: 1px solid var(--line);
}
.body :global(tbody tr:last-child th),
.body :global(tbody tr:last-child td) {
border-bottom: none;
}
.body :global(tbody tr:nth-child(even)) {
background: color-mix(in srgb, var(--paper-raised) 55%, transparent);
}
.body :global(tbody tr:hover) {
background: var(--accent-soft);
}
/* Eerste kolom vaak een label (tool / plan / model) */
.body :global(tbody th),
.body :global(tbody td:first-child) {
font-weight: 600;
color: var(--ink);
}
.body :global(td .tabnum),
.body :global(th .tabnum) {
font-variant-numeric: tabular-nums;
}
.body :global(td code),
.body :global(th code) {
font-family: var(--mono);
font-size: 0.9em;
font-weight: 500;
background: var(--paper-raised);
border: 1px solid var(--line);
border-radius: 3px;
padding: 0.1em 0.35em;
}
@media (max-width: 620px) { @media (max-width: 620px) {
.specs { grid-template-columns: 1fr 1fr; } .specs { grid-template-columns: 1fr 1fr; }
.verdict { grid-template-columns: 1fr; } .verdict { grid-template-columns: 1fr; }
.verdict .score-block { text-align: left; } .verdict .score-block { text-align: left; }
.body :global(th),
.body :global(td) {
padding: 9px 11px;
font-size: 14px;
}
.body :global(th) {
white-space: normal;
}
} }
</style> </style>