Apache Server Side Includes (SSI) are a way to mix cached secured, secured, and dynamic content on the same page without the end user knowing the difference. This is accomplished by having a tag in the form of a specialized HTML comment in the markup of the page that your Apache web server will replace with some dynamic content. This has already been integrated with Sling in the form of Sling Dynamic Includes which can replace components with SSI’s based upon resource type. However, in this article I want to dive a bit lower and look at the underlying basics of SSI from a functional perspective as well as how to best develop SSI in a local environment.
Setup
The first thing to do is to startup both an Author and Publish instance of Adobe Experience Manager. After you have done this make sure you have Docker installed and running as we will use this to containerize our Apache server.
Configure Apache Server
Now create a new directory for your Apache docker container and create a “Dockerfile” with the following contents:
FROM ubuntu:bionic RUN apt-get update && apt-get install -y apache2 RUN mkdir /var/www/html/protected WORKDIR /root ADD mod_dispatcher.so /usr/lib/apache2/modules/mod_dispatcher.so RUN chmod 644 /usr/lib/apache2/modules/mod_dispatcher.so ADD dispatcher.any /etc/apache2/conf-enabled/dispatcher.any ADD dispatcher.conf /etc/apache2/conf-available/dispatcher.conf RUN a2enconf dispatcher RUN chown -R www-data /var/www/html EXPOSE 80 443 CMD apachectl start && tail -F /var/log/apache2/error.log
This file will be used to create a docker container for our Apache web server. The “FROM” line establishes a baseline Docker container to start from. The “RUN apt-get” line installed apache. The rest of the lines setup the Apache web server and copy the project files into the Docker image. The final “CMD” line starts apache and tails the error log.
Next, create a “dispatcher.conf” file with the following contents:
LoadModule dispatcher_module /usr/lib/apache2/modules/mod_dispatcher.so LoadModule include_module /usr/lib/apache2/modules/mod_include.so <IfModule dispatcher_module> DispatcherConfig /etc/apache2/conf-enabled/dispatcher.any DispatcherLog /var/log/apache2/dispatcher.log DispatcherLogLevel debug DispatcherDeclineRoot Off DispatcherUseProcessedURL On DispatcherPassError 1 </IfModule> <Directory /var/www/html> SetHandler dispatcher-handler Order allow,deny Allow from all </Directory> <Directory /var/www/html/content/> Options Includes AddType text/html .html AddOutputFilter INCLUDES .html </Directory>
This adds the AEM dispatcher module along with the include module which makes SSI available. The first two sections configure the AEM dispatcher, the last section configures SSI for html files under /content.
Finally, add the “dispatcher.any” file and the “mod_dispatcher.so” file. These can be acquired from an AEM license. Any standard dispatcher configuration should work with one slight modification. The following needs added to the cache rules of the dispatcher.any which will prevent requests for specific components from being cached. This means that the SSI requests will always go to the publisher. If you want to cache the individual SSI requests you can skip this. This means that the content will not be dynamic but will be separated in the dispatcher cache allowing component specific cache clearing.
/0001 { /glob "/content/*/responsivegrid/*" /type "deny" }
Once all of these files are in place your directory should like like this docker-aem-dispatcher example. At this point you can go ahead and build and run docker with the following commands:
docker build -t dispatcher . docker run -it -p 8080:80 dispatcher
Create AEM Project
Now create an AEM Project using the AEM Archetype. You can use the following command or adjust versions and names as needed:
mvn archetype:generate \ -DarchetypeGroupId=com.adobe.granite.archetypes \ -DarchetypeArtifactId=aem-project-archetype \ -DgroupId=aemexamples \ -DappsFolderName=ssi \ -DarchetypeVersion=22 \ -DoptionAemVersion=6.4.0 \ -DinteractiveMode=false
Now create a file at “ui.apps/src/main/content/jcr_root/apps/ssi/components/content/ssi/ssi.html”
<sly data-sly-use.ssi="aemexamples.core.models.SSIModel"></sly> <sly data-sly-test="${! wcmmode.disabled || ssi.renderComponent}"> <p>NOT CACHED: ${ssi.randomNumber}</p> <sly data-sly-resource="${ @ path='par', resourceType='wcm/foundation/components/parsys'}"></sly> </sly> <sly data-sly-test="${wcmmode.disabled && ! ssi.renderComponent}"> <p>CACHED: ${ssi.randomNumber}</p> <!--#include virtual="${ssi.ssiIncludePath}" --> </sly>
In this file we create an instance of the yet to be created SSIModel. Then we have two sections. The first will be rendered when in any WCM mode other than disabled or when the request is for the “component scope”. Component scope means that the users request was not for the containing page, but this individual component. In this case we should render the actual markup of the component (i.e. a parsys) instead of the SSI because this is how Apache is going to request the components content. The HTML comment that starts with #include is actually the SSI itself. The “virtual” tells Apache where to look for the markup.
And create the neccessary .content.xml:
<?xml version="1.0" encoding="UTF-8"?> <jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" jcr:description="An example component using Apache server side includes." jcr:primaryType="cq:Component" jcr:title="SSI Example" allowedParents="*/parsys" componentGroup="ssi.Content"/>
And finally create a sling model at “aemexamples.core.models.SSIModel”. We will use a random number generator to show the difference between the cached and non cached parts of the page. We calculate the “component scope” concept previously mentioned by comparing the sling request path info with the resource path. Finally we get the url to pass into the SSI by appending “.html” to the resource path. In this sling model you could also check to see if the user is logged in and has authorization for this content using whatever business requirements are needed.
package aemexamples.core.models; import org.apache.sling.api.resource.Resource; import org.apache.sling.models.annotations.Model; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.models.annotations.injectorspecific.SlingObject; import org.apache.sling.models.annotations.DefaultInjectionStrategy; import javax.inject.Inject; import java.util.Random; @Model(adaptables = {SlingHttpServletRequest.class, Resource.class}, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL) public class SSIModel { @SlingObject private SlingHttpServletRequest slingHttpServletRequest; @Inject private Resource resource; public String getRandomNumber() { final Random rand = new Random(); return Integer.toString(rand.nextInt(1000)); } public boolean isRenderComponent() { // If the request resource path equals this components resource path then the request was for this component specifically. // This helps the HTL template properly render either the SSI or the parsys. return slingHttpServletRequest.getPathInfo().startsWith(resource.getPath()); } public String getSsiIncludePath() { return resource.getPath() + ".html"; } }
You can find these changes in this aem-ssi-example project.
Install this project with both of the following commands:
mvn clean install -PautoInstallPackage mvn clean install -PautoInstallPackagePublish
Create Content
Go to AEM Start > Sites > SSI > US > EN > Create Page > SSI TEST
Open the new page and add an SSI component. Author the parsys with some content and publish the page. You should see a message from the component that says, “NOT CACHED: XYZ” where “XYZ” is a random number. This is because it is not in WCM disabled mode and therefor the parsys is shown instead of the SSI.
Open the page in the publish localhost:4503 environment and inspect the markup. You should see a “CACHED” message along with the SSI.
Test the SSI
Lastly, open the page the dispatcher localhost:8080 environment. You should see both the “NOT CACHED” message and the “CACHED” message. The cached message is from a SSI. Apache replaced the SSI from the publish environment with the content that it pointed to. If you refresh the page, the entire page content outside the SSI is cache while the SSI content itself is not cached, and so you get a new random number each time.
Benefits
- SSI provides an easy way to deliver dynamic, personalized, or secure content on an otherwise cached page. However this means that non cached components should be quick to render since they will always be rendered by the publish instance.
- SSI can also be used to separate the dispatcher cache allowing for specific components such as a header to be cached separately from the rest of the page content.
- SSI is invisible to the client system. Since it is rendered by Apache before being sent to the browser, JavaScript, CSS, the browser, and search engines do not know that it is there.
Watch out
- If SSI is being used to provide dynamic or personalized content that cannot be cached ensure the capacity of the publisher to sustain the load. These components should be quick to render. If these dynamic components require complex contextual rendering within the AEM content hierarchy consider creating an application level cache of this data so that the publisher does not have to perform this logic on every request.
- SSI to some degree makes your application dependent upon the web server technology. Abstracting out the specifics of the SSI into an HTL template or a wrapping component can allow you to switch our server side includes for edge side includes, or some other technology, without affecting the rest of the application code.
Leave a Reply