diff --git a/content/posts/2024-01-23-grpc-jakarta-rs-arrays.md b/content/posts/2024-01-23-grpc-jakarta-rs-arrays.md index e297bb0..7baf1bf 100644 --- a/content/posts/2024-01-23-grpc-jakarta-rs-arrays.md +++ b/content/posts/2024-01-23-grpc-jakarta-rs-arrays.md @@ -9,7 +9,7 @@ author: Ron Sigal Release 1.0.0.Alpha5 of resteasy-grpc has a new feature for handling arbitrary arrays. Although protobuf comes with a representation of one dimension arrays, e.g. -``` +```protobuf message ints { repeated int64 is = 1; } @@ -25,7 +25,7 @@ The mechanism has two parts: arrays.proto looks like this: -``` +```protobuf message dev_resteasy_grpc_arrays___BooleanArray { repeated bool bool_field = 1; } @@ -66,7 +66,7 @@ It starts with a definition of array message types for Then, `dev_resteasy_grpc_arrays___ArrayHolder` is defined with a oneof field that can hold any of these array message types. The self-referential field -``` +```protobuf dev_resteasy_grpc_arrays___ArrayHolderArray arrayHolderArray_field = 13; ``` @@ -77,7 +77,7 @@ Compiling arrays.proto generates `dev.resteasy.grpc.arrays.Array_proto`, which g gateway into the javabuf[^javabuf] world. Suppose we want to generate a representation of `int[] {3, 5}`. That would look like -``` +```java dev_resteasy_grpc_arrays___IntArray.Builder iab = dev_resteasy_grpc_arrays___IntArray.newBuilder(); iab.addIntField(3); iab.addIntField(5); @@ -105,13 +105,13 @@ of `int[][] {{3, 5}, {7, 11, 13}}`. To avoid the mess, grpc-bridge-runtime includes the class `dev.resteasy.grpc.arrays.ArrayUtility`. With `ArrayUtility`, building the javabuf representation of `int[][] {{3, 5}, {7, 11, 13}}` is as easy as -``` +```java dev_resteasy_grpc_arrays___ArrayHolder holder = ArrayUtility.getHolder(new int[][] {{3, 5}, {7, 11, 13}}); ``` Moreover, `ArrayUtility` can turn the `dev_resteasy_grpc_arrays___ArrayHolder` back to the original array: -``` +```java Object array = ArrayUtility.getArray(holder); Assert.assertArrayEquals(new int[][] {{3, 5}, {7, 11, 13}}, (int[][]) array); ``` @@ -119,20 +119,20 @@ Moreover, `ArrayUtility` can turn the `dev_resteasy_grpc_arrays___ArrayHolder` b These two calls to `ArrayUtility` depend on the fact that the target array is built from a primitive Java type. If the array uses an application specific type, then there are two alternative calls that can be used: -``` +```java public static dev_resteasy_grpc_arrays___ArrayHolder getHolder(JavabufTranslator translator, Object o); ``` and -``` +```java public static Object getArray(JavabufTranslator translator, Array_proto.dev_resteasy_grpc_arrays___ArrayHolder ah) throws Exception; ``` Also, if an application uses arrays, the generated `JavabufTranslator` incorporates `ArrayUtility`, so that it can be used instead: -``` +```java dev_resteasy_grpc_arrays___ArrayHolder ah = (dev_resteasy_grpc_arrays___ArrayHolder) translator.translateToJavabuf(new int[][] {{3, 5}, {7, 11, 13}}); Object array = translator.translateFromJavabuf(ah); Assert.assertArrayEquals(new int[][] {{3, 5}, {7, 11, 13}}, (int[][]) array); @@ -141,7 +141,7 @@ used instead: **Note.** The latter point can be usefully expanded, independent of the presence of arrays. Consider the class -``` +```java package dev.resteasy.grpc.example; public class C { @@ -160,7 +160,7 @@ used instead: Using the fluent methods created in, say, `C_proto` by the protobuf parser, an instance of `C_proto.dev_resteasy_grpc_example___C` can be created by -``` +```java C_proto.dev_resteasy_grpc_example___C.Builder cb = C_proto.dev_resteasy_grpc_example___C.newBuilder(); C_proto.dev_resteasy_grpc_example___C c1 = cb.setI(3).setD(5.0).setS("seven").build(); ``` @@ -168,7 +168,7 @@ Using the fluent methods created in, say, `C_proto` by the protobuf parser, an i Note that each field must be set individually. On the other hand, given the `C(int, double, String)` constructor, an instance of `C_proto.dev_resteasy_grpc_example___C` can be created more directly: -``` +```java C_proto.dev_resteasy_grpc_example___C c2 = (C_proto.dev_resteasy_grpc_example___C) translator.translateToJavabuf(new C(3, 5.0, "seven")); ``` diff --git a/content/posts/2024-11-15-resteasy-spring-boot-with-springdoc.md b/content/posts/2024-11-15-resteasy-spring-boot-with-springdoc.md index 707b8af..054b597 100644 --- a/content/posts/2024-11-15-resteasy-spring-boot-with-springdoc.md +++ b/content/posts/2024-11-15-resteasy-spring-boot-with-springdoc.md @@ -20,7 +20,7 @@ The [WildFly](https://github.com/wildfly/wildfly) and [Quarkus](https://github.c - [Using MicroProfile OpenAPI With RESTEasy](https://resteasy.dev/2023/02/20/resteasy-microprofile-openapi/) -For Quarkus, here is a blog post that shows the usage of the built-in MicroProfile OpenAPI in Quarkus: +For Quarkus, here is a blog post that shows the usage of the built-in MicroProfile OpenAPI in Quarkus: - [MicroProfile OpenAPI for everyone](https://quarkus.io/blog/openapi-for-everyone/) diff --git a/content/posts/2025-02-14-resteasy-grpc-collections.md b/content/posts/2025-02-14-resteasy-grpc-collections.md index 7b25d0b..e401b7d 100644 --- a/content/posts/2025-02-14-resteasy-grpc-collections.md +++ b/content/posts/2025-02-14-resteasy-grpc-collections.md @@ -13,7 +13,7 @@ and `java.util.Set`. In order to handle arbitrary implementations, idiosyncratic implementating classes are ignored and all implementations are assigned the least common nature of lists and sets. That is, an implementation of `java.util.List` is considered to be an ordered sequence and is translated to a protobuf message type of the form -``` +```protobuf message java_util___ArrayList16 { string classname = 1; repeated int32 data = 2; @@ -21,7 +21,7 @@ message type of the form ``` and an implementation of `java.util.Set` is considered to be an unordered collection and is translated to a protobuf message type of the form -``` +```protobuf message java_util___HashSet3 { string classname = 1; repeated string data = 2; @@ -40,7 +40,7 @@ a workaround. The important thing is to define a protobuf message whose repeated `java_util___ArrayList16`. That would be a reasonable representation of `ArrayList`. Similarly, `java_util___HashSet3` would be a reasonable representation of `HashSet`. The `JavaToProtobufGenerator` class in the [grpc-bridge](https://github.com/resteasy/resteasy-grpc) module generates the protobuf messages and decorates them as follows: -``` +```java // List: java.util.ArrayList message java_util___ArrayList16 { string classname = 1; @@ -56,7 +56,7 @@ would be a reasonable representation of `HashSet`. The `JavaToProtobufGe } ``` These are two simple examples. Consider something a little more complicated: `java.util.ArrayList>`: -``` +```java // List: java.util.ArrayList> message java_util___ArrayList14 { string classname = 1; @@ -70,7 +70,7 @@ field in `java_util___ArrayList14` is of type `java_util___HashSet3`. A complication arises in the form of type variables and wildcards. The solution adopted in resteasy-grpc is to map unassigned type variables and wildcards to `java.lang.Object`, which makes sense, since they can take any types at runtime. The protobuf analog to `java.lang.Object` is `google.protobuf.Any`, which is defined -``` +```protobuf message Any { string type_url = 1; bytes value = 2; @@ -81,7 +81,7 @@ package declared in the .proto file. The value field has built-in type bytes, w of bytes no longer than 2^32", according to https://developers.google.com/protocol-buffers/docs/proto3. Suppose we have the Jakarta REST resource methods -``` +```java package x.y; @GET @@ -110,13 +110,13 @@ Suppose we have the Jakarta REST resource methods } ``` where x.y.Grimble is -``` +```java public class Grimble { T t; } ``` `JavaToProtobufGenerator` would create the rpc and message definitions -``` +```protobuf // p/grimble/raw x_y___Grimble google.protobuf.Empty GET sync rpc gr_raw (GeneralEntityMessage) returns (GeneralReturnMessage); @@ -155,7 +155,7 @@ where x.y.Grimble is ``` The details about the rpc comments are described elsewhere ([gRPC Bridge Project: User Guide](https://resteasy.dev/docs/grpc/)), but here it's enough to know that the rpc definition -``` +```protobuf // p/grimble/variable x_y___Grimble18 google.protobuf.Empty GET sync rpc gr_variable (GeneralEntityMessage) returns (GeneralReturnMessage); ``` @@ -175,7 +175,7 @@ type `google.protobuf.Any`, which, as discussed above, represents an arbitrary t `of java.lang.Object`. The discussion about generic types and type variables applies to lists and sets. For example, -``` +```java @Path("arraylist/hashset/wildcard") @POST public ArrayList> arraylistHashsetTest2(ArrayList> l) { @@ -183,7 +183,7 @@ The discussion about generic types and type variables applies to lists and sets. } ``` gives rise to -``` +```java // List: java.util.ArrayList> message java_util___ArrayList13 { string classname = 1; @@ -202,7 +202,7 @@ gives rise to Here we'll discuss a gRPC client intending to communicate with a Jakarta REST server. The subject is covered in detail in [gRPC Bridge Project: User Guide](https://resteasy.dev/docs/grpc/), but here we will look at sending and receiving `Collection`s. For example, consider the resource method -``` +```java @GET @Path("arraylist/integer") public ArrayList listArray0(ArrayList list) { @@ -212,7 +212,7 @@ Here we'll discuss a gRPC client intending to communicate with a Jakarta REST se We've seen that `java.util.ArrayList` translates to javabuf class `java_util___ArrayList16`. So the client has to create an instance of `java_util___ArrayList16` to send to the server. There are two possible strategies. One is to work in the javabuf world: -``` +```java java_util___ArrayList16.Builder juaBuilder = java_util___ArrayList16.newBuilder(); juaBuilder.setClassname("java.util.ArrayList"); juaBuilder.addData(3); @@ -220,7 +220,7 @@ work in the javabuf world: java_util___ArrayList16 jua = juaBuilder.build(); ``` Alternatively, one could create an `ArrayList` and translate it to an `java_util___ArrayList16`: -``` +```java ArrayList list = new ArrayList(); list.add(3); list.add(7); @@ -229,18 +229,18 @@ Alternatively, one could create an `ArrayList` and translate it to an `java_util ``` where `translator` is an instance of `dev.resteasy.grpc.bridge.runtime.protobuf.JavabufTranslator`. The next step is to build a `GeneralEntityMessage`: -``` +```java GeneralEntityMessage.Builder gemBuilder = GeneralEntityMessage.newBuilder(); gemBuilder.setJavaUtilArrayList16Field(gemBuilder.build()); ``` Then the remote method can be invoked: -``` +```java GeneralReturnMessage response = stub.listArray0(gem); ``` where `stub` is the client side representative of the server methods. Finally, the result can be extracted from the `GeneralReturnMessage`. Note that `listArray0` returns an instance of `ArrayList`, which translates to javabuf class -``` +```java // List: java.util.ArrayList message java_util___ArrayList17 { string classname = 1; @@ -249,11 +249,11 @@ an instance of `ArrayList`, which translates to javabuf class } ``` This complicates things a bit since we have to extract the returned list from an `Any`. -``` +```java java_util___ArrayList17 result = response.getJavaUtilArrayList17Field(); Any any = response.getAnyField(); Message result = any.unpack((Class) Utility.extractClassFromAny(any, translator)); list = (ArrayList) translator.translateFromJavabuf(result); ``` ### Maps -Stay tuned for the next release, which which implementations of `java.util.Map` will be treated in a similar way. \ No newline at end of file +Stay tuned for the next release, in which implementations of `java.util.Map` will be treated similarly. diff --git a/pom.xml b/pom.xml index 556c8c7..3bf4af6 100644 --- a/pom.xml +++ b/pom.xml @@ -33,6 +33,10 @@ + + io.quarkus + quarkus-arc + io.quarkiverse.roq quarkus-roq @@ -58,11 +62,12 @@ quarkus-roq-plugin-tagging ${version.quarkus-roq} - - io.quarkus - quarkus-arc + io.quarkiverse.roq + quarkus-roq-plugin-sitemap + ${version.quarkus-roq} + com.fasterxml.jackson.dataformat jackson-dataformat-yaml @@ -71,12 +76,13 @@ com.fasterxml.jackson.datatype jackson-datatype-jsr310 - - io.quarkiverse.roq - quarkus-roq-plugin-sitemap - ${version.quarkus-roq} + org.mvnpm + highlight.js + 11.11.1 + provided + io.quarkus quarkus-junit5 diff --git a/public/css/org_common.css b/public/css/org_common.css index 7a7c70c..99ce490 100644 --- a/public/css/org_common.css +++ b/public/css/org_common.css @@ -94,6 +94,14 @@ These are the common css styles for the JBoss.org site. /* = Common tags and elements */ +pre:has(code) { + margin: 0px; + max-height: max-content; + width: auto; + padding: 0px; + border: none; +} + pre { margin: 0px; padding: 10px; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a094ff9..1144eca 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,3 +1,7 @@ site.escaped-pages=posts/*.md,docs/**.adoc quarkus.log.category."io.quarkiverse.roq.frontmatter.deployment".level=DEBUG + +quarkus.web-bundler.dependencies.auto-import=all +quarkus.asciidoc.attributes.icons=font +quarkus.asciidoc.attributes.source-highlighter=highlight.js diff --git a/src/main/resources/web/app/highlightjs-line-numbers.js b/src/main/resources/web/app/highlightjs-line-numbers.js new file mode 100644 index 0000000..b5dd51c --- /dev/null +++ b/src/main/resources/web/app/highlightjs-line-numbers.js @@ -0,0 +1,375 @@ +// jshint multistr:true + +function installLineNumbers (hljs, w, d) { + 'use strict'; + + var TABLE_NAME = 'hljs-ln', + LINE_NAME = 'hljs-ln-line', + CODE_BLOCK_NAME = 'hljs-ln-code', + NUMBERS_BLOCK_NAME = 'hljs-ln-numbers', + NUMBER_LINE_NAME = 'hljs-ln-n', + DATA_ATTR_NAME = 'data-line-number', + BREAK_LINE_REGEXP = /\r\n|\r|\n/g; + + if (hljs) { + hljs.initLineNumbersOnLoad = initLineNumbersOnLoad; + hljs.lineNumbersBlock = lineNumbersBlock; + hljs.lineNumbersBlockSync = lineNumbersBlockSync; + hljs.lineNumbersValue = lineNumbersValue; + + addStyles(); + } else { + w.console.error('highlight.js not detected!'); + } + + function isHljsLnCodeDescendant(domElt) { + var curElt = domElt; + while (curElt) { + if (curElt.className && curElt.className.indexOf('hljs-ln-code') !== -1) { + return true; + } + curElt = curElt.parentNode; + } + return false; + } + + function getHljsLnTable(hljsLnDomElt) { + var curElt = hljsLnDomElt; + while (curElt.nodeName !== 'TABLE') { + curElt = curElt.parentNode; + } + return curElt; + } + + // Function to workaround a copy issue with Microsoft Edge. + // Due to hljs-ln wrapping the lines of code inside a element, + // itself wrapped inside a
 element, window.getSelection().toString()
+    // does not contain any line breaks. So we need to get them back using the
+    // rendered code in the DOM as reference.
+    function edgeGetSelectedCodeLines(selection) {
+        // current selected text without line breaks
+        var selectionText = selection.toString();
+
+        // get the 
' + + '' + + '' + + '', + [ + LINE_NAME, + NUMBERS_BLOCK_NAME, + NUMBER_LINE_NAME, + DATA_ATTR_NAME, + CODE_BLOCK_NAME, + i + options.startFrom, + lines[i].length > 0 ? lines[i] : ' ' + ]); + } + + return format('
element wrapping the first line of selected code + var tdAnchor = selection.anchorNode; + while (tdAnchor.nodeName !== 'TD') { + tdAnchor = tdAnchor.parentNode; + } + + // get the element wrapping the last line of selected code + var tdFocus = selection.focusNode; + while (tdFocus.nodeName !== 'TD') { + tdFocus = tdFocus.parentNode; + } + + // extract line numbers + var firstLineNumber = parseInt(tdAnchor.dataset.lineNumber); + var lastLineNumber = parseInt(tdFocus.dataset.lineNumber); + + // multi-lines copied case + if (firstLineNumber != lastLineNumber) { + + var firstLineText = tdAnchor.textContent; + var lastLineText = tdFocus.textContent; + + // if the selection was made backward, swap values + if (firstLineNumber > lastLineNumber) { + var tmp = firstLineNumber; + firstLineNumber = lastLineNumber; + lastLineNumber = tmp; + tmp = firstLineText; + firstLineText = lastLineText; + lastLineText = tmp; + } + + // discard not copied characters in first line + while (selectionText.indexOf(firstLineText) !== 0) { + firstLineText = firstLineText.slice(1); + } + + // discard not copied characters in last line + while (selectionText.lastIndexOf(lastLineText) === -1) { + lastLineText = lastLineText.slice(0, -1); + } + + // reconstruct and return the real copied text + var selectedText = firstLineText; + var hljsLnTable = getHljsLnTable(tdAnchor); + for (var i = firstLineNumber + 1 ; i < lastLineNumber ; ++i) { + var codeLineSel = format('.{0}[{1}="{2}"]', [CODE_BLOCK_NAME, DATA_ATTR_NAME, i]); + var codeLineElt = hljsLnTable.querySelector(codeLineSel); + selectedText += '\n' + codeLineElt.textContent; + } + selectedText += '\n' + lastLineText; + return selectedText; + // single copied line case + } else { + return selectionText; + } + } + + // ensure consistent code copy/paste behavior across all browsers + // (see https://github.com/wcoder/highlightjs-line-numbers.js/issues/51) + document.addEventListener('copy', function(e) { + // get current selection + var selection = window.getSelection(); + // override behavior when one wants to copy line of codes + if (isHljsLnCodeDescendant(selection.anchorNode)) { + var selectionText; + // workaround an issue with Microsoft Edge as copied line breaks + // are removed otherwise from the selection string + if (window.navigator.userAgent.indexOf('Edge') !== -1) { + selectionText = edgeGetSelectedCodeLines(selection); + } else { + // other browsers can directly use the selection string + selectionText = selection.toString(); + } + e.clipboardData.setData('text/plain', selectionText); + e.preventDefault(); + } + }); + + function addStyles () { + var css = d.createElement('style'); + css.type = 'text/css'; + css.innerHTML = format( + '.{0}{border-collapse:collapse}' + + '.{0} td{padding:0}' + + '.{1}:before{content:attr({2})}', + [ + TABLE_NAME, + NUMBER_LINE_NAME, + DATA_ATTR_NAME + ]); + d.getElementsByTagName('head')[0].appendChild(css); + } + + function initLineNumbersOnLoad (options) { + if (d.readyState === 'interactive' || d.readyState === 'complete') { + documentReady(options); + } else { + w.addEventListener('DOMContentLoaded', function () { + documentReady(options); + }); + } + } + + function documentReady (options) { + try { + var blocks = d.querySelectorAll('code.hljs,code.nohighlight'); + + for (var i in blocks) { + if (blocks.hasOwnProperty(i)) { + if (!isPluginDisabledForBlock(blocks[i])) { + lineNumbersBlock(blocks[i], options); + } + } + } + } catch (e) { + w.console.error('LineNumbers error: ', e); + } + } + + function isPluginDisabledForBlock(element) { + return element.classList.contains('nohljsln'); + } + + function lineNumbersBlock (element, options) { + if (typeof element !== 'object') return; + + async(function () { + element.innerHTML = lineNumbersInternal(element, options); + }); + } + + function lineNumbersBlockSync (element, options) { + if (typeof element !== 'object') return; + + element.innerHTML = lineNumbersInternal(element, options); + } + + function lineNumbersValue (value, options) { + if (typeof value !== 'string') return; + + var element = document.createElement('code') + element.innerHTML = value + + return lineNumbersInternal(element, options); + } + + function lineNumbersInternal (element, options) { + + var internalOptions = mapOptions(element, options); + + duplicateMultilineNodes(element); + + return addLineNumbersBlockFor(element.innerHTML, internalOptions); + } + + function addLineNumbersBlockFor (inputHtml, options) { + var lines = getLines(inputHtml); + + // if last line contains only carriage return remove it + if (lines[lines.length-1].trim() === '') { + lines.pop(); + } + + if (lines.length > 1 || options.singleLine) { + var html = ''; + + for (var i = 0, l = lines.length; i < l; i++) { + html += format( + '
' + + '
' + + '
' + + '{6}' + + '
{1}
', [ TABLE_NAME, html ]); + } + + return inputHtml; + } + + /** + * @param {HTMLElement} element Code block. + * @param {Object} options External API options. + * @returns {Object} Internal API options. + */ + function mapOptions (element, options) { + options = options || {}; + return { + singleLine: getSingleLineOption(options), + startFrom: getStartFromOption(element, options) + }; + } + + function getSingleLineOption (options) { + var defaultValue = true; + if (!!options.singleLine) { + return options.singleLine; + } + return defaultValue; + } + + function getStartFromOption (element, options) { + var defaultValue = 1; + var startFrom = defaultValue; + + if (isFinite(options.startFrom)) { + startFrom = options.startFrom; + } + + // can be overridden because local option is priority + var value = getAttribute(element, 'data-ln-start-from'); + if (value !== null) { + startFrom = toNumber(value, defaultValue); + } + + return startFrom; + } + + /** + * Recursive method for fix multi-line elements implementation in highlight.js + * Doing deep passage on child nodes. + * @param {HTMLElement} element + */ + function duplicateMultilineNodes (element) { + var nodes = element.childNodes; + for (var node in nodes) { + if (nodes.hasOwnProperty(node)) { + var child = nodes[node]; + if (getLinesCount(child.textContent) > 0) { + if (child.childNodes.length > 0) { + duplicateMultilineNodes(child); + } else { + duplicateMultilineNode(child.parentNode); + } + } + } + } + } + + /** + * Method for fix multi-line elements implementation in highlight.js + * @param {HTMLElement} element + */ + function duplicateMultilineNode (element) { + var className = element.className; + + if ( ! /hljs-/.test(className)) return; + + var lines = getLines(element.innerHTML); + + for (var i = 0, result = ''; i < lines.length; i++) { + var lineText = lines[i].length > 0 ? lines[i] : ' '; + result += format('{1}\n', [ className, lineText ]); + } + + element.innerHTML = result.trim(); + } + + function getLines (text) { + if (text.length === 0) return []; + return text.split(BREAK_LINE_REGEXP); + } + + function getLinesCount (text) { + return (text.trim().match(BREAK_LINE_REGEXP) || []).length; + } + + /// + /// HELPERS + /// + + function async (func) { + w.setTimeout(func, 0); + } + + /** + * {@link https://wcoder.github.io/notes/string-format-for-string-formating-in-javascript} + * @param {string} format + * @param {array} args + */ + function format (format, args) { + return format.replace(/\{(\d+)\}/g, function(m, n){ + return args[n] !== undefined ? args[n] : m; + }); + } + + /** + * @param {HTMLElement} element Code block. + * @param {String} attrName Attribute name. + * @returns {String} Attribute value or empty. + */ + function getAttribute (element, attrName) { + return element.hasAttribute(attrName) ? element.getAttribute(attrName) : null; + } + + /** + * @param {String} str Source string. + * @param {Number} fallback Fallback value. + * @returns Parsed number or fallback value. + */ + function toNumber (str, fallback) { + if (!str) return fallback; + var number = Number(str); + return isFinite(number) ? number : fallback; + } + +} + +export default installLineNumbers; diff --git a/src/main/resources/web/app/main.js b/src/main/resources/web/app/main.js new file mode 100644 index 0000000..fb49957 --- /dev/null +++ b/src/main/resources/web/app/main.js @@ -0,0 +1,8 @@ +import hljs from 'highlight.js'; +import installLineNumbers from './highlightjs-line-numbers.js'; +import 'highlight.js/styles/agate.css'; + +installLineNumbers(hljs, window, document); + +hljs.highlightAll(); +hljs.initLineNumbersOnLoad(); diff --git a/templates/partials/head.html b/templates/partials/head.html index e8b105a..16931ed 100644 --- a/templates/partials/head.html +++ b/templates/partials/head.html @@ -1,9 +1,7 @@ - - - - + + + {#bundle /} {#seo page site /} {#rss site /} @@ -20,5 +18,4 @@ -