├── Classes └── Aspect │ └── ContentCacheSegmentAspect.php ├── Configuration ├── Policy.yaml └── Settings.yaml ├── LICENSE ├── README.md ├── Resources ├── Private │ └── Fusion │ │ └── Root.fusion └── Public │ ├── Script │ └── main.js │ └── Style │ └── main.css ├── composer.json └── demo.gif /Classes/Aspect/ContentCacheSegmentAspect.php: -------------------------------------------------------------------------------- 1 | cacheSegmentTail = ContentCache::CACHE_SEGMENT_END_TOKEN . $randomCacheMarker; 53 | } 54 | 55 | /** 56 | * @Flow\Pointcut("setting(Yeebase.Fusion.ContentCacheDebug.enabled) && evaluate(current.securityContext.initialized == true, current.securityContext.roles contains 'Yeebase.Fusion.ContentCacheDebug:Debugger')") 57 | */ 58 | public function debuggingActive() 59 | { 60 | } 61 | 62 | /** 63 | * @Flow\Around("method(Neos\Fusion\Core\Cache\ContentCache->createCacheSegment()) && Yeebase\Fusion\ContentCacheDebug\Aspect\ContentCacheSegmentAspect->debuggingActive") 64 | * @param JoinPointInterface $joinPoint 65 | * @return string 66 | */ 67 | public function wrapCachedSegment(JoinPointInterface $joinPoint): string 68 | { 69 | $segment = $joinPoint->getAdviceChain()->proceed($joinPoint); 70 | 71 | return $this->renderCacheInfoIntoSegment($segment, [ 72 | 'mode' => static::MODE_CACHED, 73 | 'fusionPath' => $joinPoint->getMethodArgument('fusionPath'), 74 | 'entryIdentifier' => $this->interceptedCacheEntryValues, 75 | 'entryTags' => $joinPoint->getMethodArgument('tags'), 76 | 'lifetime' => $joinPoint->getMethodArgument('lifetime') 77 | ]); 78 | } 79 | 80 | /** 81 | * @Flow\Around("method(Neos\Fusion\Core\Cache\ContentCache->createUncachedSegment()) && Yeebase\Fusion\ContentCacheDebug\Aspect\ContentCacheSegmentAspect->debuggingActive") 82 | * @param JoinPointInterface $joinPoint 83 | * @return string 84 | */ 85 | public function wrapUncachedSegment(JoinPointInterface $joinPoint): string 86 | { 87 | $segment = $joinPoint->getAdviceChain()->proceed($joinPoint); 88 | 89 | return $this->renderCacheInfoIntoSegment($segment, [ 90 | 'mode' => static::MODE_UNCACHED, 91 | 'fusionPath' => $joinPoint->getMethodArgument('fusionPath'), 92 | 'contextVariables' => array_keys($joinPoint->getMethodArgument('contextVariables')) 93 | ]); 94 | } 95 | 96 | 97 | /** 98 | * @Flow\Around("method(Neos\Fusion\Core\Cache\ContentCache->createDynamicCachedSegment()) && Yeebase\Fusion\ContentCacheDebug\Aspect\ContentCacheSegmentAspect->debuggingActive") 99 | * @param JoinPointInterface $joinPoint 100 | * @return string 101 | */ 102 | public function wrapDynamicSegment(JoinPointInterface $joinPoint): string 103 | { 104 | $segment = $joinPoint->getAdviceChain()->proceed($joinPoint); 105 | 106 | return $this->renderCacheInfoIntoSegment($segment, [ 107 | 'mode' => static::MODE_DYNAMIC, 108 | 'fusionPath' => $joinPoint->getMethodArgument('fusionPath'), 109 | 'entryIdentifier' => $this->interceptedCacheEntryValues, 110 | 'entryTags' => $joinPoint->getMethodArgument('tags'), 111 | 'lifetime' => $joinPoint->getMethodArgument('lifetime'), 112 | 'contextVariables' => array_keys($joinPoint->getMethodArgument('contextVariables')), 113 | 'entryDiscriminator' => $joinPoint->getMethodArgument('cacheDiscriminator') 114 | ]); 115 | } 116 | 117 | /** 118 | * @Flow\Around("method(Neos\Fusion\Core\Cache\ContentCache->renderContentCacheEntryIdentifier()) && Yeebase\Fusion\ContentCacheDebug\Aspect\ContentCacheSegmentAspect->debuggingActive") 119 | * @param JoinPointInterface $joinPoint 120 | * @return string 121 | */ 122 | public function interceptContentCacheEntryIdentifier(JoinPointInterface $joinPoint): string 123 | { 124 | 125 | $cacheIdentifierValues = $joinPoint->getMethodArgument('cacheIdentifierValues'); 126 | $this->interceptedCacheEntryValues = []; 127 | 128 | foreach ($cacheIdentifierValues as $key => $value) { 129 | if ($value instanceof CacheAwareInterface) { 130 | $this->interceptedCacheEntryValues[$key] = $value->getCacheEntryIdentifier(); 131 | } else if (is_string($value) || is_bool($value) || is_integer($value)) { 132 | $this->interceptedCacheEntryValues[$key] = $value; 133 | } 134 | } 135 | 136 | $result = $joinPoint->getAdviceChain()->proceed($joinPoint); 137 | $this->interceptedCacheEntryValues['=>'] = $result; 138 | return $result; 139 | } 140 | 141 | /** 142 | * @Flow\Before("method(Neos\Fusion\Core\Cache\RuntimeContentCache->postProcess()) && Yeebase\Fusion\ContentCacheDebug\Aspect\ContentCacheSegmentAspect->debuggingActive") 143 | * @param JoinPointInterface $joinPoint 144 | */ 145 | public function interceptFusionObject(JoinPointInterface $joinPoint) 146 | { 147 | $this->interceptedFusionObject = $joinPoint->getMethodArgument('fusionObject'); 148 | } 149 | 150 | /** 151 | * @param string $segment 152 | * @param array $info 153 | * @return string 154 | */ 155 | protected function renderCacheInfoIntoSegment(string $segment, array $info): string 156 | { 157 | $injectPosition = 2; 158 | $info = array_slice($info, 0, $injectPosition, true) 159 | + ['fusionObject' => ObjectAccess::getProperty($this->interceptedFusionObject, 'fusionObjectName', true)] 160 | + array_slice($info, $injectPosition, count($info) - $injectPosition, true); 161 | 162 | $info['created'] = (new \DateTime())->format('d.m.Y H:i:s'); 163 | 164 | $segmentHead = substr($segment, 0, strlen($segment) - strlen($this->cacheSegmentTail)); 165 | $segmentEnd = $this->cacheSegmentTail; 166 | 167 | // Ensure we don't place comments outside of the html tag 168 | $htmlEndPosition = strpos($segmentHead, ''); 169 | if ($htmlEndPosition !== false) { 170 | $segmentEnd = substr($segmentHead, $htmlEndPosition) . $segmentEnd; 171 | $segmentHead = substr($segmentHead, 0, $htmlEndPosition); 172 | } 173 | 174 | return $segmentHead . '' . $segmentEnd; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Configuration/Policy.yaml: -------------------------------------------------------------------------------- 1 | 2 | roles: 3 | 'Yeebase.Fusion.ContentCacheDebug:Debugger': 4 | abstract: TRUE 5 | -------------------------------------------------------------------------------- /Configuration/Settings.yaml: -------------------------------------------------------------------------------- 1 | Yeebase: 2 | Fusion: 3 | ContentCacheDebug: 4 | enabled: false 5 | Neos: 6 | Neos: 7 | fusion: 8 | autoInclude: 9 | 'Yeebase.Fusion.ContentCacheDebug': false 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 yeebase media GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yeebase.Fusion.ContentCacheDebug 2 | 3 | The Yeebase.Fusion.ContentCacheDebug package is a helper package to visualize your cache configuration. Once the package is active, administrators can view a cache configuration overlay on the website to see exactly which parts of the website are cached like 4 | 5 | 6 | ## Installation & configuration 7 | 8 | Install the package via composer 9 | ``` 10 | composer require yeebase/fusion-contentcachedebug 11 | ``` 12 | 13 | The debug mode is disabled by default. To enable it add this to your Settings.yaml 14 | 15 | ```yaml 16 | Yeebase: 17 | Fusion: 18 | ContentCacheDebug: 19 | enabled: true 20 | 21 | ``` 22 | 23 | Now the package is active and will render some metadata in your html output if the current user 24 | inherits the role `Yeebase.Fusion.ContentCacheDebug:Debugger`. Only user with this role will be able to see the debug information. 25 | 26 | To get the debugger running you now need to include some javascript and css to acutally be able to render the output. For Neos we already adjusted the `Neos.Neos.Page` prototype. Include this in your Root.fusion of your site package: 27 | ``` 28 | include: resource://Yeebase.Fusion.ContentCacheDebug/Private/Fusion/Root.fusion 29 | ``` 30 | 31 | If you're running a fusion standalone app check that code and include it the js and css files to your page. 32 | 33 | ## Usage 34 | To enable the cache visualization open your browsers developer console and execute 35 | `__enable_content_cache_debug__()`. This will add three new buttons. 36 | 37 | 🔦 toggle visualization 38 | 39 | 📋 displays a list of used cached entries in a hierarchical order 40 | 41 | ❌ disable debug mode 42 | 43 | If you'd like to persist the active debug state you can add a `true` to the method 44 | ``` 45 | __enable_content_cache_debug__(true) 46 | ``` 47 | This will set a cookie and the debug mode will still be active after a page refresh. 48 | 49 | ![Demo](demo.gif) 50 | -------------------------------------------------------------------------------- /Resources/Private/Fusion/Root.fusion: -------------------------------------------------------------------------------- 1 | prototype(Neos.Neos:Page) { 2 | head.contentCacheDebugStyle = Neos.Fusion:Tag { 3 | tagName = 'link' 4 | attributes { 5 | rel = 'stylesheet' 6 | href = Neos.Fusion:ResourceUri { 7 | path = 'resource://Yeebase.Fusion.ContentCacheDebug/Public/Style/main.css' 8 | } 9 | } 10 | 11 | @if.notInBackend = ${!documentNode.context.inBackend} 12 | @if.isActive = ${Configuration.setting('Yeebase.Fusion.ContentCacheDebug.enabled')} 13 | @if.onlyAdmins = ${Security.hasRole('Yeebase.Fusion.ContentCacheDebug:Debugger')} 14 | } 15 | 16 | contentCacheDebugScript = Neos.Fusion:Tag { 17 | tagName = 'script' 18 | attributes { 19 | src = Neos.Fusion:ResourceUri { 20 | path = 'resource://Yeebase.Fusion.ContentCacheDebug/Public/Script/main.js' 21 | } 22 | } 23 | 24 | @if.notInBackend = ${!documentNode.context.inBackend} 25 | @if.isActive = ${Configuration.setting('Yeebase.Fusion.ContentCacheDebug.enabled')} 26 | @if.onlyAdmins = ${Security.hasRole('Yeebase.Fusion.ContentCacheDebug:Debugger')} 27 | @position = 'before closingBodyTag' 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Resources/Public/Script/main.js: -------------------------------------------------------------------------------- 1 | window.__enable_content_cache_debug__ = (setCookie = false) => { 2 | if (window.__enable_content_cache_debug__.active) { 3 | return; 4 | } 5 | 6 | if (setCookie) { 7 | document.cookie = '__content_cache_debug__=true'; 8 | } 9 | 10 | console.log('%c ', 'color: white; background: #00ADEE; line-height: 20px; font-weight: bold'); 11 | 12 | window.__enable_content_cache_debug__.active = true; 13 | 14 | const PREFIX = '__CONTENT_CACHE_DEBUG__'; 15 | const mouseOffset = 5; 16 | 17 | const treeWalker = document.createTreeWalker( 18 | document.getRootNode(), 19 | NodeFilter.SHOW_COMMENT, 20 | node => node.nodeValue.indexOf(PREFIX) === 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP, 21 | false 22 | ); 23 | 24 | const nodes = []; 25 | while (treeWalker.nextNode()) { 26 | nodes.push(treeWalker.currentNode); 27 | } 28 | 29 | const infoElements = []; 30 | const createInfoElement = ({ parentNode, cacheInfo, index }) => { 31 | 32 | const container = document.createElement('div'); 33 | container.classList.add('content-cache-debug-container'); 34 | 35 | const button = document.createElement('button'); 36 | button.innerText = '💡'; 37 | container.appendChild(button); 38 | 39 | const overlay = document.createElement('div'); 40 | overlay.classList.add(cacheInfo.mode); 41 | container.appendChild(overlay); 42 | 43 | const table = document.createElement('table'); 44 | table.classList.add('content-cache-debug-table'); 45 | 46 | const clone = parentNode.cloneNode(); 47 | clone.innerHTML = ''; 48 | cacheInfo['markup'] = clone.outerHTML.replace(/<\/.+/, '').replace(//g, '>').substring(0, 50); 49 | 50 | Object.keys(cacheInfo).forEach(key => { 51 | const tr = document.createElement('tr'); 52 | const th = document.createElement('th'); 53 | const td = document.createElement('td'); 54 | 55 | th.innerText = key; 56 | tr.classList.add(key.toLowerCase()); 57 | 58 | let value = cacheInfo[key]; 59 | 60 | if (value === null) { 61 | return; 62 | } 63 | 64 | let arrayValue; 65 | if (Array.isArray(value)) { 66 | arrayValue = value.map((v, i) => `${i}: ${v}`); 67 | } else if (typeof value === 'object') { 68 | arrayValue = Object.keys(value).map(subKey => `${subKey}: ${value[subKey]}`); 69 | } 70 | 71 | if (arrayValue) { 72 | if (arrayValue.length > 10) { 73 | arrayValue = arrayValue.slice(0, 5).concat(['....']).concat(arrayValue.slice(arrayValue.length - 5)); 74 | } 75 | td.innerHTML = arrayValue.join('
'); 76 | } else { 77 | td.innerHTML = value; 78 | } 79 | 80 | tr.appendChild(th); 81 | tr.appendChild(td); 82 | table.appendChild(tr); 83 | }); 84 | 85 | let left = 0; 86 | let top = 0; 87 | 88 | const positionTable = event => { 89 | let x = event.pageX - left + mouseOffset; 90 | let y = event.pageY - top + mouseOffset; 91 | 92 | const rightEdge = x + table.offsetWidth + left - window.scrollX; 93 | if (rightEdge > window.innerWidth) { 94 | x -= rightEdge - window.innerWidth; 95 | } 96 | 97 | const bottomEdge = y + table.offsetHeight + top - window.scrollY; 98 | if (bottomEdge > window.innerHeight) { 99 | y -= bottomEdge - window.innerHeight; 100 | } 101 | 102 | table.setAttribute('style', `left: ${x}px; top: ${y}px`); 103 | } 104 | 105 | button.addEventListener('click', () => { 106 | container.classList.add('removed'); 107 | }); 108 | 109 | button.addEventListener('mouseenter', event => { 110 | button.innerText = '❌'; 111 | container.appendChild(table); 112 | positionTable(event); 113 | }); 114 | 115 | button.addEventListener('mousemove', positionTable); 116 | 117 | button.addEventListener('mouseleave', () => { 118 | container.removeChild(table); 119 | button.innerText = '💡'; 120 | }); 121 | 122 | infoElements.push({ 123 | container, 124 | cacheInfo, 125 | table, 126 | show: () => { 127 | let { x, y, width, height } = parentNode.getBoundingClientRect(); 128 | 129 | x += window.scrollX; 130 | y += window.scrollY; 131 | 132 | if (y < 0) { 133 | height += y; 134 | y = 0; 135 | } 136 | 137 | left = x; 138 | top = y; 139 | document.body.prepend(container); 140 | container.setAttribute('style', `width: ${width}px; height: ${height}px; left: ${x}px; top: ${y}px`); 141 | }, 142 | hide: () => { 143 | table.remove(); 144 | container.remove(); 145 | } 146 | }); 147 | }; 148 | 149 | nodes.forEach((node, index) => { 150 | const cacheInfo = JSON.parse(node.nodeValue.substring(PREFIX.length)); 151 | const parentNode = node.previousElementSibling; 152 | createInfoElement({ parentNode, cacheInfo, index }); 153 | }); 154 | 155 | infoElements.sort((a, b) => { 156 | const fa = a.cacheInfo.fusionPath; 157 | const fb = b.cacheInfo.fusionPath; 158 | 159 | if (fa < fb) return -1; 160 | if (fa > fb) return 1; 161 | return 0; 162 | }); 163 | 164 | const cacheTable = (() => { 165 | const container = document.createElement('div'); 166 | container.classList.add('content-cache-debug-list'); 167 | 168 | infoElements.forEach(({ cacheInfo, table }) => { 169 | 170 | const positionTable = event => { 171 | let x = event.pageX + mouseOffset - window.scrollX + container.scrollLeft; 172 | let y = event.pageY + mouseOffset - window.scrollY + container.scrollTop; 173 | 174 | const rightEdge = x + table.offsetWidth - container.scrollLeft; 175 | if (rightEdge > window.innerWidth) { 176 | x -= rightEdge - window.innerWidth; 177 | } 178 | 179 | const bottomEdge = y + table.offsetHeight - container.scrollTop; 180 | if (bottomEdge > window.innerHeight && table.offsetHeight < window.innerHeight) { 181 | y -= bottomEdge - window.innerHeight; 182 | } 183 | 184 | table.setAttribute('style', `left: ${x}px; top: ${y}px`); 185 | }; 186 | 187 | const div = document.createElement('div'); 188 | div.classList.add(cacheInfo.mode); 189 | div.innerHTML = cacheInfo.fusionPath.replace(/\//g, '/').replace(/<([^>\/]{2,})>/g, '<$1>'); 190 | 191 | div.addEventListener('mouseenter', event => { 192 | container.appendChild(table); 193 | positionTable(event); 194 | }); 195 | 196 | div.addEventListener('mousemove', positionTable); 197 | 198 | div.addEventListener('mouseleave', () => { 199 | table.remove(); 200 | }); 201 | 202 | container.appendChild(div); 203 | 204 | }); 205 | 206 | return { 207 | show: () => document.body.appendChild(container), 208 | hide: () => container.remove() 209 | } 210 | })(); 211 | 212 | let infoVisible = false; 213 | let listVisible = false; 214 | 215 | const shelf = document.createElement('div'); 216 | shelf.classList.add('content-cache-debug-shelf'); 217 | 218 | const infoButton = document.createElement('button'); 219 | infoButton.innerText = '🔦'; 220 | 221 | let reposition = null; 222 | const onScroll = () => { 223 | console.log('now'); 224 | if (reposition === null) { 225 | infoElements.forEach(e => e.hide()); 226 | } 227 | clearTimeout(reposition); 228 | reposition = setTimeout(() => { 229 | infoElements.forEach(e => e.show()); 230 | reposition = null; 231 | }, 200); 232 | }; 233 | 234 | infoButton.addEventListener('click', () => { 235 | if (infoVisible) { 236 | infoElements.forEach(e => e.hide()); 237 | window.removeEventListener('scroll', onScroll); 238 | } else { 239 | infoElements.forEach(e => e.show()); 240 | window.addEventListener('scroll', onScroll); 241 | } 242 | infoVisible = !infoVisible; 243 | }); 244 | 245 | shelf.appendChild(infoButton); 246 | 247 | const listButton = document.createElement('button'); 248 | listButton.innerText = '📋'; 249 | listButton.addEventListener('click', () => { 250 | if (listVisible) { 251 | cacheTable.hide(); 252 | } else { 253 | cacheTable.show(); 254 | } 255 | listVisible = !listVisible; 256 | }); 257 | 258 | shelf.appendChild(listButton); 259 | 260 | const closeButton = document.createElement('button'); 261 | closeButton.innerText = '❌'; 262 | 263 | closeButton.addEventListener('click', () => { 264 | shelf.remove(); 265 | infoVisible && infoElements.forEach(e => e.hide()); 266 | listVisible && cacheTable.hide(); 267 | window.__enable_content_cache_debug__.active = false; 268 | document.cookie = "__content_cache_debug__=; expires=Thu, 01 Jan 1970 00:00:00 UTC;"; 269 | console.log('%c
', 'color: white; background: #00ADEE; line-height: 20px; font-weight: bold'); 270 | }); 271 | 272 | shelf.appendChild(closeButton); 273 | 274 | document.body.appendChild(shelf); 275 | }; 276 | 277 | (() => { 278 | const cookies = document.cookie.split(';').map(v => v.trim()).reduce((c, s) => { 279 | const p = s.split('='); 280 | c[p[0]] = p[1]; 281 | return c; 282 | }, {}); 283 | 284 | if (cookies.__content_cache_debug__ === "true") { 285 | window.onload = window.__enable_content_cache_debug__ 286 | } 287 | })(); 288 | -------------------------------------------------------------------------------- /Resources/Public/Style/main.css: -------------------------------------------------------------------------------- 1 | .content-cache-debug-container { 2 | position: absolute; 3 | z-index: 10000; 4 | pointer-events: none; 5 | outline: 2px solid black; 6 | outline-offset: -5px; 7 | } 8 | 9 | .content-cache-debug-container:hover { 10 | z-index: 10001; 11 | } 12 | 13 | .content-cache-debug-container.removed { 14 | display: none; 15 | } 16 | 17 | .content-cache-debug-table { 18 | pointer-events: none; 19 | background: black; 20 | color: white; 21 | font-size: 12px; 22 | position: absolute; 23 | table-layout: fixed; 24 | } 25 | 26 | .content-cache-debug-table th { 27 | font-weight: normal; 28 | text-align: left; 29 | vertical-align: top; 30 | background: black; 31 | padding: auto; 32 | color: lightgray; 33 | } 34 | 35 | .content-cache-debug-table td { 36 | word-wrap: break-word; 37 | background: black; 38 | padding: auto; 39 | color: lightgray; 40 | } 41 | 42 | .content-cache-debug-table i { 43 | font-style: normal; 44 | background: rgba(255 ,255, 255, 0.4); 45 | padding: 0 4px; 46 | border-radius: 2px; 47 | } 48 | 49 | .content-cache-debug-shelf button, 50 | .content-cache-debug-container button { 51 | position: absolute; 52 | top: 5px; 53 | left: 5px; 54 | font-size: 1.5rem; 55 | border: none; 56 | background: black; 57 | width: 30px; 58 | height: 30px; 59 | display: flex; 60 | justify-content: center; 61 | pointer-events: all; 62 | } 63 | 64 | .content-cache-debug-container div { 65 | position: absolute; 66 | top: 0; 67 | left: 0; 68 | width: 100%; 69 | height: 100%; 70 | outline-offset: -3px; 71 | outline-style: solid; 72 | outline-width: 3px; 73 | } 74 | 75 | .content-cache-debug-container div.cached { 76 | outline-color: rgb(0, 255, 0); 77 | } 78 | 79 | .content-cache-debug-container button:hover ~ div.cached { 80 | background-color: rgba(0, 255, 0, 0.2); 81 | } 82 | 83 | .content-cache-debug-container div.uncached { 84 | outline-color: rgb(255, 0, 0); 85 | } 86 | 87 | .content-cache-debug-container button:hover ~ div.uncached { 88 | background-color: rgba(255, 0, 0, 0.2); 89 | } 90 | 91 | .content-cache-debug-container div.dynamic { 92 | outline-color: rgb(255, 255, 0); 93 | } 94 | 95 | .content-cache-debug-container button:hover ~ div.dynamic { 96 | background-color: rgba(255, 255, 0, 0.2); 97 | } 98 | 99 | .content-cache-debug-list { 100 | position: fixed; 101 | top: 0; 102 | right: 0; 103 | bottom: 0; 104 | left: 0; 105 | z-index: 10002; 106 | background: rgba(0, 0, 0, 0.9); 107 | color: white; 108 | padding: 40px 0; 109 | font-size: 12px; 110 | overflow: auto; 111 | display: flex; 112 | flex-direction: column; 113 | align-items: flex-start; 114 | } 115 | 116 | .content-cache-debug-list > div { 117 | margin-bottom: 5px; 118 | padding: 0 5px; 119 | cursor: pointer; 120 | } 121 | 122 | .content-cache-debug-list > div.cached { 123 | background: rgba(0 ,255, 0, 0.4); 124 | } 125 | 126 | .content-cache-debug-list > div.uncached { 127 | background: rgba(255 ,0, 0, 0.4); 128 | } 129 | 130 | .content-cache-debug-list > div.dynamic { 131 | background: rgba(255 ,255, 0, 0.4); 132 | } 133 | 134 | .content-cache-debug-list > div i { 135 | font-style: normal; 136 | font-weight: bold; 137 | color: darkgrey; 138 | padding: 0 5px; 139 | } 140 | 141 | .content-cache-debug-list > div span { 142 | font-weight: bold; 143 | color: rgb(50, 50, 50); 144 | padding: 0 0 0 3px; 145 | } 146 | 147 | .content-cache-debug-list table tr.fusionpath { 148 | display: none; 149 | } 150 | 151 | .content-cache-debug-shelf { 152 | position: fixed; 153 | top: 0; 154 | z-index: 10003; 155 | display: flex; 156 | width: 100%; 157 | justify-content: center; 158 | pointer-events: none; 159 | } 160 | 161 | .content-cache-debug-shelf button { 162 | position: static; 163 | margin: 0 3px; 164 | } 165 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yeebase/fusion-contentcachedebug", 3 | "description": "Helper package to visualize fusion content cache", 4 | "license": "MIT", 5 | "type": "neos-yeebase", 6 | "require": { 7 | "neos/fusion": "~3.3.0 || ~4.0" 8 | }, 9 | "autoload": { 10 | "psr-4": { 11 | "Yeebase\\Fusion\\ContentCacheDebug\\": "Classes/" 12 | } 13 | }, 14 | "extra": { 15 | "neos": { 16 | "package-key": "Yeebase.Fusion.ContentCacheDebug", 17 | "loading-order": { 18 | "after": [ "neos/neos" ] 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeebase/Yeebase.Fusion.ContentCacheDebug/0fe9565ee79eb14908a4ba3147370f0891e8c451/demo.gif --------------------------------------------------------------------------------