Instructions for AI, generic version.

Digital Domain Technologies Ltd Consultancy has an improved version. Contact tom@digitaldomaintechnologies.com

*** New Page ***
https://www.aem.live/developer/tutorial

Getting Started – Developer Tutorial
This tutorial will get you up-and-running with a new Adobe Experience Manager (AEM) project. In ten to twenty minutes, you will have created your own site and be able to create, preview, and publish your own content, styling, and add new blocks.
Prerequisites:
You have a GitHub account, and understand Git basics.
You have a Google account.
You understand the basics of HTML, CSS, and JavaScript.
You have Node/npm installed for local development.
This tutorial uses macOS, Chrome, and Visual Studio Code as the development environment and the screenshots and instructions reflect that setup. You can use a different operating system, browser, and code editor, but the UI you see and steps you must take may vary accordingly.
Get started with the boilerplate repository template
The fastest and easiest way to get started following AEM best practices is to create your repository using the Boilerplate GitHub repository as a template.https://github.com/adobe/aem-boilerplate
Click the Use this template button and select Create a new repository, and select the user org that owns the repository
We recommend that the repository is set to public.
The only remaining step in GitHub is to install the AEM Code Sync GitHub App on your repository by visiting this link: https://github.com/apps/aem-code-sync/installations/new
In the Repository access settings of the AEM Code Sync App, make sure you select Only select Repositories (not All Repositories). Then select your newly created repository, and click Save.
Note: If you are using Github Enterprise with IP filtering, you can add the following IP to the allow list: 3.227.118.73
Congratulations! You have a new website running on https://<branch>--<repo>--<owner>.aem.page/ In the example above that’s https://main--mysite--aemtutorial.aem.page/
Link your own content source using Google Drive
In your fork of the Boilerplate GitHub repository, the site points to an existing content source in Google Drive. See this folder for some example content.
This content is read-only, but it can be copied into your Google Drive folder to serve as a starting point.
To author your own content, create a folder in your own Google Drive and share the folder with the Adobe Experience Manager user (helix@adobe.com).
A good way to start authoring your own content is to copy index, nav and footer from the sample content and familiarize yourself with the content structure. nav and footer are not changed frequently in a project and have a special structure. Most of the files in a project look more similar to index.
Open the files and copy/paste the entire content into corresponding files in your own Google Drive. You can also download the files via Download All or download individual files. However, remember to convert the downloaded .docx files back into native Google Docs, when you upload them to your folder in your Google Drive.
Now that you have your content, you need to connect that content to your GitHub repo. You do this by changing the reference in fstab.yaml in your GitHub repo to the folder you just shared.Copy/paste the folder URL from your Google Drive to fstab.yaml.
Be aware that after you make that change, you will see 404 not found errors as your content has not been previewed yet. Please refer to the next section to see how to start authoring and previewing your content. If you copied over index, nav and footer all three of those are separate documents with their own preview and publish cycles, so make sure you preview (and publish) all of them if needed.
Commit your changes and you have hooked up your own content source to your website.
Preview and publish your content
After completing the last step, your new content source is not empty, but no content has been promoted to the preview or live stages, which means your website serves 404s.To preview content, an author has to install the Sidekick Chrome extension. Find the Chrome extension in the Chrome Web Store.
After adding the extension to Chrome, don’t forget to pin it, this will make it easier to find it.
To set up the Chrome extension, go to your previously shared Google Drive folder and click the extension icon in the browser toolbar and select Add this project.
As soon as the extension is installed and your project is added, you are ready to preview and publish your content from your Google Drive.Select all three docs and activate the AEM Sidekick by clicking on your pinned extension. A new toolbar will appear. Clicking the preview or publish buttons will trigger the corresponding operation.
Open the index doc and make some changes. Activate the Sidekick by clicking on your pinned extension and then click the Preview button which will trigger the preview operation and open a new tab with the preview rendition of the content.
Start developing styling and functionality
To get started with development, it is easiest to install the AEM Command Line Interface (CLI) and clone your repo locally through using the following.
From there change into your project folder and start your local development environment using the following.
This opens http://localhost:3000/ and you are ready to make changes.A good place to start is in the blocks folder which is where most of the styling and code lives for a project. Simply make a change in a .css or .js and you should see the changes in your browser immediately.
Once you are are ready to push your changes, simply use Git to add, commit, and push and your code to your preview (https://<branch>--<repo>--<owner>.aem.page/) and production (https://<branch>--<repo>--<owner>.aem.live/) sites.
That’s it, you made it! Congrats, your first site is up and running. If you need help in the tutorial, please join our Discord channel or get in touch with us.

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/anatomy-of-a-franklin-project

The Anatomy of a Project
This document describes what a typical project should look like from a code standpoint. Before reading this document, please familiarize yourself with the document Getting Started - Developer Tutorial.
Git and GitHub
One of our defining philosophies is that it is easiest to allow users to work with the tools that they are familiar with. The overwhelming majority of developers manage their code in systems based on Git, so it only makes sense to allow developers to work with Git to manage and deploy their code.
We are using a buildless approach that runs directly from your GitHub repo. After installing the AEM GitHub bot on your repo, websites are automatically created for each of your branches for content preview on https://<branch>--<repo>--<owner>.hlx.page/ and the production site on https://<branch>--<repo>--<owner>.hlx.live/ for published content.
Every resource that you put into your GitHub repo is available on your website, so a file in your GitHub repo on the main branch in /scripts/scripts.js will be available on https://main--<repo>--<owner>.hlx.page/scripts/scripts.jsThis should be very intuitive. There are few “special” files that Adobe Experience Manager uses to connect the content into your website.
We strongly recommend that repos are kept public on GitHub, to foster the community. For a public facing website there really is no need to keep the code hidden, as it is being served to the browsers of your website.
Important notes:
The combination <branch>--<repo>--<owner> must not exceed 63 characters (including the hyphens/dashes). This is a subdomain name constraint.
branch, repo and owner cannot contain --.
Configuration Files
There are some files in your GitHub repo that have a special meaning to AEM and are processed in a special fashion. These files are in the root directory of your repo.
The Content Connection: fstab.yaml
As you may have seen in the getting started guide the fstab.yaml file serves as the connection to your Google Drive or SharePoint folders that contain your content. This file is used as an indicator for how the code in your GitHub repo gets combined with the content in your content source.
Beyond providing the connection to the content, the fstab.yaml also provides a folder mapping facility that allows you map extensionless “routes” to a given piece of content or static HTML.
The Entry Point: head.html
The head.html file is the most important extension point to influence the markup of the content. The easiest way to think of it is that this file is injected on the server side as part of the <head> HTML tag and is combined with the metadata coming from the content.
The head.html should remain largely unchanged from the boilerplate and there are only very few legitimate reasons in a regular project to make changes. Those include remapping your project to a different base URL for the purposes of exposing your project in a different folder than the root folder of your domain on your CDN or to support legacy browsers which usually require scripts that are not loaded as modules.
Adding marketing technology like Adobe Web SDK, Google Tag Manager or other 3rd party scripts to your head.html file is strongly advised against due to performance impacts. Adding inline scripts or styles to head.html is also not advisable for performance and code management reasons, see the section Scripts and Styles below for more information about handling scripts and styles.
Please see the following examples.
https://github.com/adobe/helix-project-boilerplate/blob/main/head.html
https://github.com/adobe/express-website/blob/main/head.html
https://github.com/adobe/business-website/blob/main/head.html
Not Found: 404.html
To create a custom 404 response, place a 404.html file into the root of your github repository. This will be served on any URL that doesn’t map to an existing resource in either content or code, and replaces the body of the out of the box minimalist 404 response.
The 404 can mimic the markup of an existing page including your code for the site, with navigation footers etc., or it can have a completely different appearance.
https://github.com/hlxsites/bamboohr-website/blob/main/404.html See in Action
https://github.com/adobe/design-website/blob/main/404.html See in Action
https://github.com/adobe/blog/blob/main/404.html See in Action
Don’t Serve: .hlxignore
There are some files in your repo that should not be served from your website, either because you would like to keep them private or they are not relevant to the delivery of the website (e.g. tests, build tools, build artifacts, etc.) and don’t need to be observed by the AEM bot. You can add those to a .hlxignore file in the same format as the well-known .gitignore file.
Please see the following example.
https://github.com/adobe/helix-website/blob/main/.hlxignore
Tame the Bots: robots.txt
A robots.txt file is generally a regular file that is served as you would expect on your production website on your own domain. To protect your preview and origin site from being indexed, your .page and .live sites will serve a robots.txt file that disallows all robots instead of the robots.txt file from your repo.
https://github.com/adobe/blog/blob/main/robots.txt
https://github.com/adobe/pages/blob/main/robots.txt
https://github.com/adobe/helix-website/blob/main/robots.txt
Query and Indexing: helix-query.yaml
There is a flexible indexing facility that lets you keep track of all of your content pages conveniently as a spreadsheet. This facility is often used to show lists or feeds of pages as well as to filter and sort content on a website. Please see the document Indexing for more information.
https://github.com/adobe/express-website/blob/main/helix-query.yaml
https://github.com/adobe/design-website/blob/main/helix-query.yaml
https://github.com/adobe/blog/blob/main/helix-query.yaml
Automate Your Sitemap: helix-sitemap.yaml
Complex sitemaps can automatically be created for you whenever authors publish new content, including flexible hreflang mappings where needed. This functionality is usually based on the indexing facility.See this GitHub issue for sitemap configuration options.
https://github.com/adobe/pages/blob/main/helix-sitemap.yaml
https://github.com/adobe/fedpub/blob/main/helix-sitemap.yaml
https://github.com/adobe/express-website/blob/main/helix-sitemap.yaml
Commonly Used File and Folder Structure
Beyond the files treats as special or configuration files, there is a commonly-used structure that is expressed in the Boilerplate repo.
The common folders below are usually in the root directory of a project repo, but in cases where only a portion of a website is handled by AEM, they are often moved to a subfolder to reflect the mapping of the route of the origin in a CDN.
This means that in a case where for example only /en/blog/ is initially mapped to AEM from the CDN, all the folder structures below (eg. /scripts, /styles, /blocks etc.) are moved into a the /en/blog/ folder in GitHub to keep the CDN mapping as simple as possible.
With a simple adjustment of the reference to scripts.js and styles.css in head.html (see above) it is possible to indicate that all the necessary files are loaded from the respective code base directory. To avoid url rewriting the folder structure is also created the content source (eg. sharepoint or google drive) by having a directory structure of /en/blog/.In many cases as the AEM footprint grows on a site there is a point in time when the code gets moved back to the root folder and the head.html references are adjusted accordingly.
Scripts and Styles
By convention in a AEM project, the head.html references styles.css, scripts.js, and lib-aem.js located in /scripts and /styles, as the entry points for the project code.
scripts.js is where your global custom javascript code lives and is where the block loading code is triggered. styles.css hosts the global styling information for your site, and minimally contains the global layout information that is needed to display the Largest Contentful Paint (LCP).As all three files are loaded before the page can be displayed, it is important that they are kept relatively small and executed efficiently.Beyond styles.css, a lazy-styles.css file is commonly used, which is loaded after the LCP event, and therefore can contain slower/more CSS information. This could be a good place for fonts or global CSS that is below the fold.
In addition to scripts.js, there is the commonly-used delayed.js. This is a catch-all for libraries that need to be loaded on a page but should be kept from interfering with the delivery of the page. This is a good place for code that is outside of the control of your project and usually includes the martech stack and other libraries.
Please see the document Keeping it 100, Web Performance for more information about optimizing your site performance.
Blocks
Most of the project-specific CSS and JavaScript code lives in blocks. Authors create blocks in their documents. Developers then write the corresponding code that styles the blocks with CSS and/or decorates the DOM to take the markup of a block and transform it to the structure that’s needed or convenient for desired styling and functionality.
The block name is used as both the folder name of a block as well as the filename for the .css and .js files that are loaded by the block loader when a block is used on a page.
The block name is also used as the CSS class name on the block to allow for intuitive styling. The javascript is loaded as a module (ESM) and exports a default function that is executed as part of the block loading.
A simple example is the Columns Block. It adds additional classes in JavaScript based on how many columns are in the respective instance created by the author. This allows it to be able to use flexible styling of content that is in two columns vs. three columns.
Icons
Most projects have SVG files that are usually added to the /icons folder, and can be referenced with a :<iconname>: notation by authors. By default, icons are inlined into the DOM so they can be styled with CSS, without having to create SVG symbols.

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/block-collection

Block Collection
This is a collection of blocks considered a part of the AEM product and are recommended as blueprints for blocks in your project.
These blocks come from real production AEM projects. To be a part of this collection, a block needs to have a high use across a number of projects and provide enough abstract functionality and be general enough so it can be reused without having to change the underlying content model.
As the needs and designs of websites change, the block collection will change as well. Additions will be made to reflect emerging needs of projects, but blocks that are not used frequently enough will also be removed (deprecated).
There are few technical principles for the blocks in the collection:
Intuitive: Content structure that’s intuitive and easy to author
Useable: No dependencies, compatible with boilerplate
Responsive: Works across all breakpoints
Context Aware: Inherits CSS context such text and background colors
Localizable: No hard-coded content
Fast: No negative performance impact
SEO and A11y: SEO friendly and accessible
All of the blocks can be considered as a basis for your own block development. It is very likely that you will change all the .css and .js code to meet your own project needs. The primary value of these blocks is the content structure they provide.Considering that the code of your block will be fully adapted to your project, there is no intent for the blocks in the collection to be backwards compatible to their respective older versions or to make them upgradable.
Boilerplate
The most commonly used blocks (as well as default content types) are curated in the AEM Boilerplate and are a part of every AEM project. For a block to become a part of boilerplate it has to be used by the vast majority of all AEM projects.
The code base for all the blocks in AEM Boilerplate is open-source and can be found on GitHub adobe/aem-boilerplate
Blocks in AEM Boilerplate can be discovered using the sidekick library below, use the copy button to copy the corresponding content structure into your clipboard and paste into a document to see the content structure.
Headings
Default Content
Different levels of headings provide the semantic backbone of your document
Text
Body text or copy with rich semantic formatting options
Images
Pictures bring your content alive
Lists
Ordered and unordered lists wherever they are needed
Links
Reference other websites or your own content
Buttons
Call-to-action buttons and more
Code
Highlight preformatted code snippets in your content
Sections
Group content on your page into sections
Make your content more interesting with icons
Hero
Block
Hero treatment at the top of a page
Columns
Flexible way to handle multi-column layouts in a responsive way
Cards
List of cards with or without images and links
Header
Flexible header and navigation example
Footer
Simple extensible footer block
Metadata
Add metadata to your page where needed
Section Metadata
Highlight or structure all the content in a section
The block collection contains blocks that are commonly-used, but are not so common to be considered boilerplate. As a rule-of-thumb, to be included in the block collection a block must be used on more than half of all AEM projects.
The block collection can be the entry path into boilerplate code. Likewise if a block in the boilerplate is no longer used as much, it can be moved to this collection.
The code base for all the blocks in AEM Block Collection is open-source and can be found on GitHub adobe/aem-block-collection
Blocks in AEM Block Collection can be discovered using the sidekick library below, use the copy button to copy the corresponding content structure into your clipboard and paste into a document to see the content structure.
Embed
A simple way to embed social media content into AEM pages
Fragment
Share pieces of content across multiple pages
Table
A way to organize tabular data into rows and columns
Video
Display and playback videos directly from AEM
Accordion
A stack of descriptive labels that can be toggled to display related full content
Breadcrumbs
Block Add-on
A list of page titles and relevant links showing the location of the current page in the navigational hierarchy
Carousel
A dynamic display tool that smoothly transitions through a series of images with optional text content
Modal
Autoblock
A popup that appears over other site content
Quote
A display of a quotation or a highlight of specific passage (or “pull quotes”) within a document
Search
Allows users to find site content by entering a search term
Tabs
Segment information into multiple labeled (or “tabbed”) panels
Form
Block (Deprecated)
A set of input controls grouped together that enables users to submit information
The block collection is continually evolving based on the feedback from the AEM community. If you think that there is a block that should be included in the block collection please speak to your AEM contact. Current candidates for inclusion in the block collection include:
Consent Banner
If you have immediate need of a block that is not yet part of the collection, it is relatively easy to find AEM projects on GitHub that have example implementations for all of the above candidates.
Block Party
The Block Party is a place for the AEM developer community to showcase what they have built on AEM sites. It also allows others to avoid reinventing the wheel and reuse these blocks / code snippets / integrations built by the community and tweak the code as necessary to fit their own projects. See Block Party for everything it has to offer.
Note: While we love and support our AEM developer community, Adobe is not responsible for maintaining or updating the code that is showcased in Block Party. Please use the code at your own discretion.

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/spreadsheets

Spreadsheets and JSON
In addition to translating Google Docs and Word documents into markdown and HTML markup, AEM also translates spreadsheets (Microsoft Excel workbooks and Google Sheets) into JSON files that can easily be consumed by your website or web application.
This enables many uses for content that is table-oriented or structured.
Sheets and Sheet structure
The simplest example of a sheet consists of a table that uses the first row as column names and the subsequent rows as data. An example might look something like this.
After a preview and publish via the sidekick, AEM translates this table to a JSON representation which is served to requests to the corresponding .json resource. The above example gets translated to:
AEM allows you to manage workbooks with multiple sheets.
If there is only one sheet, AEM will by default use that sheet as the source of the information.
If there are multiple sheets, AEM will only deliver sheets that are prefixed with helix- which lets you keep additional information and possibly formulas in the same spreadsheet that are not delivered to the web.
If there is a sheet named helix-default, it is delivered if there are no additional query parameters supplied.
See the following section for details on how to query a specific sheet.
Query Parameters
Offset and Limit
Spreadsheets and JSON files can get very large. In such cases, AEM supports the use of limit and offset query parameters to indicate which rows of the spreadsheet are delivered.
As AEM always compresses the JSON, payloads are generally relatively small. Therefore by default AEM limits the number of rows it returns to 1000 if the limit query parameter is not specified. This is sufficient for many simple cases.
Sheet
The sheet query parameter allows an application to specify one or multiple specific sheets in the spreadsheet or workbook. As an example ?sheet=jobs will return the sheet named helix-jobs and ?sheet=jobs&sheet=articles will return the data for the sheets named helix-jobs and helix-articles.
Special Sheet Names
In certain use cases, AEM also writes to spreadsheets, where it expects specific sheet names.
The forms service only writes to a sheet named incoming, which is never delivered as a JSON.
The index service only writes to a sheet named raw_index, which may be delivered to JSON in a simple single sheet setup.
See the links above for more information on those services.
Arrays
Native arrays are not supported as cell values, so they are delivered as strings.
"tags": "[\"Adobe Life\",\"Responsibility\",\"Diversity & Inclusion\"]"
You can turn them back into arrays in JavaScript using JSON.parse().

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/indexing

Indexing
Adobe Experience Manager offers a way to keep an index of all the published pages in a particular section of your website. This is commonly used to build lists, feeds, and enable search and filtering use cases for your pages or content fragments.
AEM keeps this index in a spreadsheet and offers access to it using JSON. Please see the document Spreadsheets and JSON for more information.
We will introduce the concept of creating a query index by previewing an Excel workbook or Google spreadsheet first. Note, that if you already have a custom query definition in a file called helix-query.yaml in your GitHub repository, it is no longer possible to create indexes that way. Every new index will have to be manually added to that helix-query.yaml.
Setting Up an Initial Query Index
In this section we’ll create a query index in the root folder that will index all documents in your backend.
After setting up your fstab.yaml with a mountpoint that points into your SharePoint site or Google Drive, go to the root folder.
Depending on your backend, create either a workbook named query-index.xlsx for SharePoint or a spreadsheet named query-index for Google Drive.
In that spreadsheet or workbook, create a sheet named raw_index.
Setting Up Properties to be Added to the Index
In your query-index document, add a header line and in the first column add path as the header name.
In the following columns of the header line, add all other properties you need extracted from the rendered HTML page.
In the following example in Google Drive, the extracted fields are title, image, description, and lastModified.
Pages are indexed when they are published. To remove pages from index, they have to be unpublished.
For simple scenarios without custom index definition, pages that have robots metadata property set to noindex will automatically be omitted from indexing by AEM. (There are a few special scenarios here, for more details see the section Special Scenarios for Robots).
The following table summarizes the properties that are available and from where in the HTML page they’re extracted.
Returns the content of the meta tag named article:tag in the head element as an array.
See the document Spreadsheets and JSON for more information on array-handling.
For every other header added, the indexer will try to find a meta tag with a corresponding name.
Activate Your Index
To activate your index, preview the spreadsheet using the sidekick. This will create an index configuration.
Checking Your Index
The Admin Service has an API endpoint where you can check the index representation of your page. Given your GitHub owner, repository, branch and owner, and a resource path to a page, its endpoint is:
https://admin.hlx.page/index/<owner>/<repo>/<branch>/<path>
You should get a JSON response where the data node contains the index representation of the page.
Debugging Your Index Configuration
The AEM CLI has a feature where it will print the index record whenever you change your query configuration, which assists in finding the correct CSS selectors:
$ aem up --print-index
Please see the AEM CLI GitHub documentation for more information and watch this video to learn more about this feature.
Setting Up More Index Configurations
You can define your own custom index configurations by creating your own helix-query.yaml. This allows you to have more than one index configuration in the same helix-query.yaml, where parts of your sites are indexed into different Excel workbooks or Google spreadsheets. See the document Indexing reference for more information.
Special Scenarios for Robots
There are a few nuances on how pages get indexed by AEM in conjunction with indexing setup for your site. Let’s look at them:
In the following 2 situations, setting robots to noindex on the page metadata would not prevent it from being indexed by AEM:
You have added a robots column in query-index.xlsx
You have a helix-query.yaml in your Github repository i.e. you have defined a custom index definition.
Recommendations
If you do not have a custom index definition, it is recommended to not add a robots column to your index sheet unless you have a requirement for doing so.Adding robots column to your index sheet would cause a page to be indexed by AEM even though it may have robots metadata set to noindex.
If you do have a custom index definition, pages would get indexed by AEM irrespective of setting robots to noindex on the page metadata. If you want to prevent this from happening, you can use spreadsheet filters to omit pages from index that have robots metadata set to noindex. For more details, see the section titled "Enforcing noindex configuration with custom index definitions" below.
Enforcing “noindex” configuration with custom index definitions
If you have defined your own custom index definitions in helix-query.yaml, setting the robots property to noindex is not effective in preventing the pages from getting indexed. In order to enforce noindex configuration is such situations, do the following:
Create a sheet named “helix-default” in your query-index.xlsx . After this, your query-index.xlsx spreadsheet should have 2 sheets “raw_index” and “helix-default”. The “raw_index” sheet is there to have all the raw indexed data.
Modify your custom helix-query.yaml (it must be in your project’s Github repository) and add the robots property so that it gets indexed.
Now set up your “helix-default” sheet in the query-index.xlsx spreadsheet to get automatically filled up using Excel formula which ensures that all the rows in raw_index which have robots property set as noindex, do not get copied over to the helix-default sheet. This can be done by using an Excel formula like this =FILTER(Table1,NOT(Table1[robots]="noindex"))
Now your helix-default sheet has only the rows from raw_index that do not have robots property set to noindex.
Ensure that you publish the pages that you want to get indexed.
Now if you fetch the index as usual like: https://<branch>--<repo>-<org>.hlx.page/query-index.json, you’d only get data from helix-default sheet i.e. entries that are not explicitly prevented from getting indexed through the robot property set as noindex.

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/keeping-it-100

Web Performance, Keeping your Lighthouse Score 100.
The quality of the experience of websites is crucial to achieving the business goals of your website and the satisfaction of your visitors.
Adobe Experience Manager (AEM) is optimized to deliver excellent experiences and optimal web performance. With the Real Use Monitoring (RUM) operations data collection, information is continuously collected from field use and offers a way to iterate on real use performance measurements without having to wait for the CRuX data to show effects of code and deployment changes. It is common for field data collected in RUM to deviate from the lab results, as the network, geo-location and processing power for real devices are much more diverse than the simulated conditions in a lab.
The Google PageSpeed Insight Service is proven to be a great lab measurement tool. It can be used to avoid the slow deterioration of the performance and experience score of your website.
If you start your project with the Boilerplate as in the Developer Tutorial, you will get a very stable Lighthouse score on PageSpeed Insight for both Mobile and Desktop at 100. On every component of the lighthouse score there is some buffer for the project code to consume and still be within the boundaries of a perfect 100 lighthouse score.
Testing Your Pull Requests
It turns out that it is hard to improve your Lighthouse score once it is low, but it is not hard to keep it at 100 if you continuously test.
When you open a pull request (PR) on a project, the test URLs in the description of your project are used to run the PageSpeed Insights Service against. The AEM GitHub bot will automatically fail your PR if the score is below 100 with a little bit of buffer to account for some volatility of the results.
The results are for the mobile lighthouse score, as they tend to be harder to achieve than desktop.
Why Google PageSpeed Insights?
Many teams and individuals use their own configurations for measuring Lighthouse scores. Different teams have developed their own test harnesses and use their own test tools with configurations that have been set up as part of their continuous monitoring and performance reporting practices.
The performance of a website impacts its rankings in search results, which is reflected by the Core Web Vitals in the crUX report. Google has a great handle on the relevant average combinations of device information (e.g. screen size) as well as network performance of those devices. But in the end, SEO is the ultimate arbiter of what good vs. bad web performance is. As the specific configuration is a moving target, performance practices should be aligned with the current average devices and network characteristics globally.
So instead of using a project specific configuration for Lighthouse testing, we use the continuously-updated configurations seen as part of the mobile and desktop strategies referenced in the latest versions of the Google PageSpeed Insights API.
While there may be additional insight that some developers feel they can collect from other ways of measuring Lighthouse scores, to be able to have a meaningful and comparable performance conversation across projects, there needs to be a way to measure performance universally. The default PageSpeed Insight Service is the most authoritative, most widely accepted lab test when it comes to measuring your performance.However it is important to remember that the recommendations that you get from PageSpeed Insights do not necessarily lead to better results, especially the closer you get to a lighthouse score of 100.Core Web Vitals (CWV) collected by the built-in RUM data collection play an important role in validating results. For minor changes, however, the variance of the results and the lack of sufficient data points (traffic) over a short period of time makes it impractical to get statistically relevant results in most cases.
Three-Phase Loading (E-L-D)
Dissecting the payload that's on a web page into three phases makes it relatively straight-forward to achieve a clean lighthouse score and therefore set a baseline for a great customer experience.
The three phase loading approach divides the payload and execution of the page into three phases
Phase E (Eager): This contains everything that's needed to get to the largest contentful paint (LCP).
Phase L (Lazy): This contains everything that is controlled by the project and largely served from the same origin.
Phase D (Delayed): This contains everything else such as third-party tags or assets that are not material to experience.
Phase E: Eager
In the eager phase, everything that's needed to be loaded for the true LCP to be displayed is loaded. In a project, this usually consists of the markup, the CSS styles and JavaScript files.
In many cases the LCP element is contained in a block (often created by auto blocking), where the block .js and and .css also have to be loaded.
The block loader unhides sections progressively, which means that all the blocks of the first section have to be loaded for the LCP to become visible. For this reason, it might make sense to have a smaller section containing as little as needed at the top of a page.
It is a good rule of thumb to keep the aggregate payload before the LCP is displayed below 100kb, which usually results in an LCP event quicker than 1560ms (LCP scoring at 100 in PSI). Especially on mobile the network tends to be bandwidth constrained, so changing the loading sequence before LCP has minimal to no impact.
Loading from or connecting to a second origin before the LCP occurred is strongly discouraged as establishing a second connection (TLS, DNS, etc.) adds a significant delay to the LCP.
Phase L: Lazy
In the lazy phase, the portion of the payload is loaded that doesn't affect total blocking time (TBT) and ultimately first input delay (FID).
This includes things like loading blocks (JavaScript and CSS) as well as loading all the remaining images according to their loading="lazy" attribute and other JavaScript libraries that are not blocking. The lazy phase is generally everything that happens in the various blocks you are going to create to cover the project needs.
In this phase it would still be advisable that the bulk of the payload come from the same origin and is controlled by the first party, so that changes can be made if needed to avoid negative impact on TBT, TTI and FID.
Phase D: Delayed
In the delayed phase, the parts of the payload are loaded that don't have an immediate impact to the experience and/or are not controlled by the project and come from third parties. Think of marketing tooling, consent management, extended analytics, chat/interaction modules etc. which are often deployed through tag management solutions.
It is important to understand that for the impact on the overall customer experience to be minimized, the start of this phase needs to be significantly delayed. The delayed phase should be at least three seconds after the LCP event to leave enough time for the rest of the experience to get settled.
The delayed phase is usually handled in delayed.js which serves as an initial catch-all for scripts that cause TBT. Ideally, the TBT problems are removed from the scripts in question either by loading them outside of the main thread (in a web worker) or by just removing the actual blocking time from the code. Once the problems are fixed, those libraries can easily be added to the lazy phase and be loaded earlier.
Ideally there is no blocking time in your scripts, which is sometimes hard to achieve as commonly used technology like tag managers or build tooling create large JavaScript files that are blocking as the browser is parsing them. From a performance perspective it is advisable to remove those techniques, make sure your individual scripts are not blocking and load them individually as separate smaller files.
Header and Footer
The header and specifically the footer of the page are not in the critical path to the LCP, which is why they are loaded asynchronously in their respective blocks. Generally, resources that do not share the same life cycle (meaning that they are updated with authoring changes at different times) should be kept in separate documents to make the caching chain between the origins and the browser simpler and more effective. Keeping those resources separate increases cache hit ratios and reduces cache invalidation and cache management complexity.
Fonts
Since web fonts are often a strain on bandwidth and loaded from a different origin via a font service like https://fonts.adobe.com or https://fonts.google.com, it is largely impossible to load fonts before the LCP, which is why they are usually added to the lazy-styles.css and are loaded after the LCP is displayed.
LCP Blocks
There are situations where the actual LCP element is not included in the markup that is transmitted to the client. This happens when there is an indirection or lookup (for example a service that’s called, a fragment that’s loaded or a lookup that needs to happen in a .json) for the LCP element.In those situations, it is important that the page loading waits with guessing the LCP candidate (currently the first image on the page) until the first block has made the necessary changes to the DOM.To identify which blocks to wait for before blocking for the LCP load, you can add the blocks that contain the LCP element to the LCP_BLOCKS array in scripts.js.
Bonus: Speed is Green
Building websites that are fast, small, and quick to render is not just a good idea to deliver exceptional experiences that convert better, it is also a good way to reduce carbon emissions.

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/markup-sections-blocks

Markup, Sections, Blocks, and Auto Blocking
To design websites and create functionality, developers use the markup and DOM that is rendered dynamically from the content. The markup and DOM are constructed in a way that allows flexible manipulation and styling. At the same time it provides out-of-the-box functionality so the developer does not have to worry about some of the aspects of modern websites.
Structure of a Document
The single most important aspect when structuring a document is to make it simple and intuitive for authors who will contribute the content.
This means that it is strongly recommended to involve authors very early in the process. In many cases it is good practice to just let authors put the content that needs to go on to a page into a Google Doc or Word document without having any notion of blocks and section, and then try to make small structural changes and introduce sections and blocks only where necessary.
A document follows the following structure in the abstract.
A page as authored in Word or a Google Doc document uses the well-understood semantic model like headings, body text, lists, images, links, etc. that is shared between HTML, markdown, and Google Doc / Word. We call this default content. In an ideal situation one would leave as much of the content authored as default content as possible, since this is the natural way for authors to treat documents.In addition to default content, we have a concept of page sections, separated by horizontal rules or --- to group certain elements of a page together. There may be both semantic and design reasons to group content together. A simple case could be that a section of a page has a different background color.In addition to that there is concept of blocks which are authored as a table with a heading as the first row that identifies the type of block. This concept is the easiest approach to componentize your code.
Sections can contain multiple blocks. Blocks should never be nested as it makes things very hard to use for authors.
DOM vs. Markup
AEM produces a clean and easily readable semantic markup from the content that’s provided to it. You can easily access it using the view source feature and have a look at the markup of the page you are currently reading.
The JavaScript library used in scripts.js takes the markup and enhances it into a DOM that is then used for most development tasks, specifically to build blocks. To create a DOM that’s easy to work with for custom project code, it is best to view it as a two-step process.
In the first step, we create the markup with sections, blocks, and default content that will look similar to this.
In the second step, the above mark-up is augmented into the following example DOM, which then can be used for styling and adding functionality. The differences between the markup that’s delivered from the server and the augmented DOM that is used for most of the development tasks is highlighted below.
It primarily consists of introducing a wrapper <div> for blocks and default content and dynamically adding additional helpful CSS classes and data attributes that are used by the AEM block loader.
Sections are a way to group default content and blocks by the author. Most of the time section breaks are introduced based on visual differences between sections such as a different background color for a part of a page.
From a development perspective, there is usually not much interaction with sections beyond CSS styling.Sections can contain a special block called Section Metadata, which results in data attributes to a section. The names of the data attributes can be chosen by the authors, and the only well-known section metadata property is Style which will be turned into additional CSS classes added to the containing section element.
Blocks and default content are always wrapped in a section, even if the author doesn’t specifically introduce section breaks.
There is a broad range of semantics that are shared between Word documents, Google Docs, markdown, and HTML. For example there are headings of different levels (eg. <h1> - <h6>), images, links, lists (<ul>, <ol>), emphasis (<em>, <strong>) etc.
We take advantage of the intuitive understanding that authors have of how to use these semantics in the tools that they are familiar with (eg. Word/Google docs) and maps those to markdown and then renders them in the HTML markup.
All mappings should be relatively straightforward and intuitive for the developer. One area where we go a little bit further than the simplest possible translation is in handling images. Instead of a simple <img> tag, a full <picture> tag is rendered with a number of different resolutions needed for display on desktop and mobile devices as well as different formats for modern browsers that support webp and older browsers which do not.
Most of the project-specific CSS and JavaScript lives in blocks. Authors create blocks in their documents and developers write the corresponding code that styles the blocks with CSS and/or decorates the DOM to take the markup of a block and transform it to the structure that’s needed or convenient for desired styling and functionality.
The block name is used as both the folder name of a block as well as the filename for the CSS and JavaScript files that are loaded by the block loader when a block is used on a page. The block name is also used as the CSS class name on the block to allow for intuitive styling.
The JavaScript is loaded as a Module (ESM) and exports a default function that is executed as part of the block loading.
All block level CSS should be scoped to the block to make sure that there are no side-effects for other parts of your project, which means that all selectors in a block should be prefixed with the corresponding block class. In certain cases it makes sense to use the block’s wrapper or containing section for the selector as well.
There is a balance of DOM manipulation in JavaScript and complexity of the CSS selectors. Complex brittle CSS selectors are not recommended and at the same time adding classes to every element makes your code more complex and disregards the semantics of elements.
One of the most important tenets of a project is to keep things simple and intuitive for authors. Complicated blocks make it hard to author content, so it is important that developers absorb the complexity of translating an intuitive authoring experience into the DOM that is needed for layout or application logic. It is often tempting to delegate complexity to the author. Instead, developers should make sure that blocks do not become unwieldy to create for authors. An author should always be able to simply copy/paste a block and intuitively understand what it is about.
Blocks can be very simple or contain full application components or widgets and provide a way for the developer to componentize their codebase into small chunks of code that can be managed easily and can be loaded on to the web pages as needed.
A block’s content is rendered into the markup as nested <div> tags for the rows and columns that the author entered. In the simplest case, a block has only a single cell.
Authors can add blocks to their pages in an ad-hoc manner by simply adding a table with the block name in the first row or table heading. Some blocks are also loaded automatically. header and footer blocks that need to be present on every page of a site are a good example of that.
Block Options
If you need a block to look or behave slightly differently based on certain circumstances, but not different enough to become a new block in itself, you can let authors add block options to blocks in parentheses. These options add modified classes to the block. For example Columns (wide) in a table header will generate the following markup.
<div class=”columns wide”>
Block options can also contain multiple words. For example Columns (super wide) will be concatenated using hyphens.
<div class=”columns super-wide”>
If block options are comma-separated, such as Columns (dark, wide), they will be added as separate classes.
<div class=”columns dark wide”>
Auto Blocking
In an ideal scenario the majority of content is authored outside of blocks, as introducing tables into a document makes it harder to read and edit. Conversely blocks provide a great mechanism for developers to keep their code organized.
A frequently-used mechanism to get the best of both worlds is called auto blocking. Auto blocking turns default content and metadata into blocks without the author having to physically create them. Auto blocking happens very early in the page decoration process before blocks are loaded and is a practice that programmatically creates the DOM structure of a block as it would come as markup from the server.
Auto blocking is often used in combination with metadata, particularly the template property. If pages have a common template, meaning that they share a certain page design or functionality, that’s usually a good opportunity for auto blocking.
A good example is an article header of a blog post. It might contain information about the author, the title of the blog post, a hero image, as well as the publication date. Instead of having the author put together a block that contains all that information, an auto block (e.g. article-header block) would be programmatically added to the page based on the <h1>, the first image, the blog author, and publication date metadata.
This allows the content author to keep the information in its natural place, the document structure outside of a block. At the same time, the developer can keep all the layout and styling information in a block.
Another very common use case is to wrap blocks around links in a document. A good example is an author linking to a YouTube video by simply including a link, while the developer would like to keep all the code for the video inline embed in an embed block.
This mechanism can also be used as a flexible way to include both external applications and internal references to video, content fragments, modals, forms, and other application elements.
The code for your projects auto blocking lives in buildAutoBlocks() in your scripts.js.
Please see the following examples of auto blocking.
Adobe Blog
AEM Boilerplate

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/favicon

Favicon
Adding a favicon to your site gives it a professional look in your visitor’s browsers:
Adding a favicon
Add a file called favicon.ico to the root folder in your GitHub repository. We recommend using the .ico format for best support across all major browsers. That’s all – your site now has a favicon!

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/sitemap

Sitemaps
Create automatically generated sitemap files to be referenced from your robots.txt. This helps with SEO and the discovery of new content. AEM can generate three types of sitemaps: without any configuration, based solely on a query index or based on a manual sitemap configuration.
Creating a Sitemap without any configuration
If you don’t do anything you will see your sitemap in sitemap.xml and have a sitemap index in sitemap.json. It will contain a list of all your published documents.
If you started with another type of sitemap and would like to switch to this type, you’ll have to delete the helix-sitemap.yaml configuration file - either manually defined in GitHub or automatically generated - and reindex your site.
Domain name used in external URLs
To customize the domain used in creating external URLs, add a property named host or cdn.prod.host in your project configuration (named .helix/config when using Google Drive as backend or .helix/config.xlsx on Sharepoint) and preview that file to activate it.
Generating a Sitemap configuration based on an index
Please see the document Indexing to learn more about indexing. In order to generate a sitemap configuration based on an index, please ensure that you have already set up an initial query index as explained there. This will generate a sitemap at the location:
https://<branch>--<repo>--<owner>.hlx.page/sitemap.xml
And a sitemap configuration at the following location:
https://<branch>--<repo>--<owner>.hlx.page/helix-sitemap.yaml
It is recommended that you create a sitemap-index.xml file that references all your sitemaps and keep that as part of your project code in your github repo. This way it is easy to add new sitemaps as the project expands.
Manual setup of your Sitemap configuration
If you need more customization than your generated sitemap configuration file provides, you can copy its contents and paste it into a file named helix-sitemap.yaml in the root folder of your project.
Note: When using a manually configured index and sitemap (e.g. your code repo includes a helix-query.yaml and helix-sitemap.yaml file) your index definition must include the robots property to ensure the sitemap excludes pages with robots: noindex metadata. When using auto-generated index definitions, simply follow the recommendations in the indexing documentation so those pages are excluded from the index.
The following sections contain the supported types of sitemaps.
Simple Sitemap
The following is a simple helix-sitemap.yaml. It assumes a single index containing all the pages that need to appear in the sitemap.
If you want last modification dates to be included in the URLs of your sitemap, add a lastmod property including a format to your configuration.
Multiple Sitemaps
It is common to have sitemaps per section of the sites and/or per country or language. AEM supports sitemaps including the corresponding hreflang references. In the following example we assume that there is a one to one mapping between the indexes and the sitemaps XML files.
If there are two pages in the english and french section that share a common suffix, they will be related, so e.g. if you have a page /welcome in the english section and a page /fr/welcome in the french section, the resulting entry in the /sitemap-en.xml will look like this:
A similar entry will be available in /sitemap-fr.xml.
Specifying the primary language manually
There might be situations where you have alternate versions of a page, but you’re unable to use a common suffix to identify them, possibly because you’re porting a legacy website that should not have its paths changed. In that situation, you can specify a primary-language-url for the alternate location, in the metadata of the document.
Let’s assume our primary language is english, we have a page /welcome in the english section and /fr/bienvenu in the french section, and the latter is an alternate version of the former.
First, we add that information to the document at /fr/bienvenu in its metadata:
This can also be added to a global metadata sheet, as shown in Bulk Metadata.
Then, we add an indexed property primary-language-url to the french index:
Finally, we re-publish the french page, and rebuild the sitemap.
Specifying the default language
Another common requirement is to specify the default language for a sitemap with multiple languages. This can be achieved by adding a property default in the sitemap:
In the resulting sitemap, all entries from the english subtree will have an extra alternate entry with hreflang x-default.
Specifying multiple hreflangs for one subtree
Sometimes, it is required to map multiple hreflangs to only one language subtree, e.g. consider we want the following to appear in the resulting sitemap:
Every page in our sitemap source should appear exactly once, but have multiple alternate hreflangs associated with it. In order to achieve this, you should specify an array of languages in the hreflang property:
Multiple Indexes Aggregated Into One Sitemap
There are cases where it is easier to have a single larger sitemap than fragmented small sitemaps, especially as there is a limit of sitemaps that can be submitted to search engines per site.
The following example shows how to aggregate a number of separate indexes into a single sitemap.
Using the same destination it is possible to combine multiple small sitemaps into one larger sitemap.
Including other sitemaps as input
In a mixed scenario, where not all languages in a sitemap are managed in AEM, you can include sitemaps from other language trees by specifying an XML path as source, as in:
In this example, we use an external french sitemap to calculate all sitemap locations. AEM will determine alternates for english sitemap URLs by deconstructing the french counterparts in external sitemap using the alternate definition.
Adding an extension to all locations in the sitemap
In a scenario, where you want all your locations to have an extension, e.g. .html, and you’re unable to generate a helix-sitemap sheet in your query index to derive a formula, you can add an extension to all languages or an individual language using an extension property:

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/forms

Forms
Understanding the available forms blocks
Document-based Authoring (Microsoft Excel, Google Sheets)
AEM Authoring (Adaptive Forms Editor, Universal Editor)
WYSIWYG editing experience for easy form creation and visualization
(Deprecated) Submission to Microsoft Excel and Google Sheets
Submission to Microsoft Excel and Google Sheets
Submit data to databases, content management systems (CMSs), External APIs
Pre-processing and post-processing data for data cleaning, data combining, data transformation, and data reduction.
Email Notification on form submission
Form Data Model for defining data structure and interactions with various data sources
Configure forms to produce submit data in predefined and structured format.
Ability to choose from built-in submit actions for handling form submissions, including submitting data to Microsoft SharePoint, Microsoft OneDrive, Adobe Workfront Fusion, Salesforce, Microsoft Dynamics, many more data sources.
Basic HTML input types
All basic HTML5 input types
File Upload
Terms & Condition
Form Container
Wizard
Forms Portal components to list all the forms on an AEM Sites page
Ability to create custom form components for specific needs
Google reCAPTCHA
hCaptcha
Cloudflare Turnstile
Integration with Adobe Sign for electronic signatures
Integration with Adobe Workfront Fusion to trigger Adobe Workfront Fusion scenarios upon form
Integration with Adobe Analytics to capture user interaction data
Integration with AEM Workflow
Integration with various data sources for pre-populating forms and submitting data.
Show/hide fields
Compute value
Enable or disable an object
Set a value for an object
Clear the value of an object
Set focus on a specific object
Submit or reset the form entirely or a particular object
Validate the form or an object
Advanced rules editor for creating complex logic
Server-side extensibility for custom functionalities
Document of record functionality to create archives of submitted data
For advanced users, there's also support of custom functions for more granular control.
For more details about Adaptive Forms Block, check out AEM Forms Edge Delivery Services documentation. An example of a basic form block can be found here.
(Deprecated) Preparing a Sheet for Data Ingestion
AEM offers a forms service that ingests submitted data into a Microsoft Excel or Google Sheet document.
Having data submitted through forms on your website flow directly into spreadsheets makes it easily accessible to business users. Such data can also interact with more complex automated workflows as both Google Sheets as well as Microsoft Excel and Sharepoint offer access to a vibrant ecosystem and robust APIs.
Create an Excel workbook or Google sheet anywhere under your project directory. This document uses a sheet in OneDrive called email-form.xlsx in the root of an AEM project.
Make sure the AEM user (for example helix@adobe.com) that is configured for your project has edit permission on the sheet.
Open the workbook created and rename the default sheet to incoming.
Note: AEM will not send any data to this workbook if the incoming sheet does not exist.
Preview the sheet in the sidekick.
Note: Even if a sheet has been previewed previously it must be previewed again after creating the incoming sheet for the first time.
Set up the sheet with the headers that match the data being ingested. You can either do this manually or by sending a POST request to the form route in the AEM Admin service. The admin service will look at the data in the POST body and create the necessary headers/tables and sheets required to ingest data and get the most from the forms service.For details on the format of the POST request to set up your sheet, see the Admin API documentation as well as the following example
POST /form/{owner}/{repo}/{ref}/en/email-form.json HTTP/1.1
{"data":{"firstName":"test"}}
Response:
HTTP/1.1 200 OK
{"rowCount":2,"columns":["firstName"]}
You can use a tool like curl or Postman to make this POST request. For example:
curl -s -i -X POST 'https://admin.hlx.page/form/{owner}/{repo}/{ref}/en/email-form.json' \
--header 'Content-Type: application/json' \
--data '{"data":{"firstName":"test"}}'
Through the above POST request, we are providing sample data i.e. the form fields and sample values which will be used by the Admin service for setting up the form.The Admin service is recommended to set up your sheet, but if you would like to create the headers manually, please see the document Manual Forms Sheet Setup.
After sending the POST request to the admin service you will see the following changes to your workbook.
A sheet named helix-default is created. The data in this sheet is what will be returned when a GET request is made to the sheet. This is a great place to use spreadsheet formulas to summarize the data from the incoming sheet for consumption elsewhere.Note: The helix-default sheet should never contain any personally identifiable information or other data you are not ok with being publicly accessible.
A sheet named slack was created. Here you can set up automatic notifications to a Slack channel anytime data is ingested to your spreadsheet. Currently AEM only supports notifications to the AEM Engineering slack org as well as the Adobe Enterprise Support org.
To setup Slack notifications enter the teamId of the Slack workspace and the channel name or ID. You can also ask the slack-bot (with the debug command) for the teamId and the channel ID. Using the channel ID instead of the name is preferable, as it survives channel renames.Note: Older forms didn’t have the teamId column. The teamId was included in the channel column, separated by a # or /.
Enter any title you want and under fields enter the names of the fields you want to see in the Slack notification. Each heading should be separated by a comma (eg. name, email).
(Deprecated) Sending Data to Your Sheet
The sheet is now ready for data ingestion and you can send POST requests directly to the sheet on hlx.page, hlx.live or your production domain.
Note: The URL should not include the .json extension. The sheet must be published for POST operations to work on .live or on the production domain.
There are a few different ways you can format the form data in the POST body.
As an array of name/value pairshttps://gist.github.com/dylandepass/9ba6b83700dfce1fa90a47bde62c2e9
As an object with key/value pairshttps://gist.github.com/dylandepass/2b5f694723dfdb3d304fcafc613d6595
As x-www-form-urlencoded body (content-type header must be set to application/x-www-form-urlencoded)https://gist.github.com/dylandepass/b72b2e30313bc80beb02e12b1d7201ff
That’s it! The forms service runs every minute so you will quickly see your data ingested into the sheet.
Author-Created Forms
In many cases it is desirable to have authors create forms and decide what form fields should be presented to the visitor of your website. It is common to use the helix-default sheet of the same spreadsheet that is used for the submission of the form as the place to let the author define their forms.
Usually, there is a forms block that takes a reference to the spreadsheet and renders the form and handles the user flow through submission.
A simple example of such a basic form block can be found here. You can also use the Adaptive Forms Block to develop forms.
See the following example of what the spreadsheet for the form definition could look like.
The supported form fields are extensible and the form should be viewed as an example that gives you a starting point.
As an example a fully-functional form was added to this page, using the previously-listed code base by simply adding the following block in the Google Doc:
Feel free to try it out and see the form data flow into the incoming sheet. It may take a minute to get from the form service to the spreadsheet.

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/block-collection/headings

Notes:
Semantic headings are the backbone of any document structure. In documents you should always follow the semantic hierarchy of your document, meaning that a Heading 1 should contain a Heading 2 which in turn should contain a Heading 3 and so forth.In cases where you find yourself using the headings out of sequence or leaving gaps in the heading hierarchy, that’s usually an indication that you are either trying to use headings to adjust to visual or design constraints or you are using headings for something that is semantically not a heading. Either of those can lead to bad results.
According to Web best practices there should only be a single Heading 1 per page, which will also be used as the default title for the document.
Example:
See Live output
Content Structure:
The Content Structure leverages the built-in Heading 1 - Heading 6 mapped to h1 through h6.
See Content in Document
Code:
As headings are default content they are styled in project or block CSS code. There is usually no JavaScript code used.
There is no code related to list generic styling in Boilerplate.

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/block-collection/text

A Text Paragraph (or Copy) is the most common element on websites. AEM understands and translates a number of semantic formatting like bold, italic, underline, strike-through as well as subscript and superscript, which are translated to their respective semantic HTML tags of <strong>, <em>, <u>, <s>, <sup> and <sub>.
CSS styling may take hints from these formatting options and use them to express visually very different styling. Both a paragraph and line-feed are supported.
The first portion of the first text paragraph serves as the default description for the a page if nothing else is specified in metadata.
The content structure is based on the simple Normal Text paragraphs in word or google doc.
As text is Default Content it is styled in project or block CSS code. There is usually no JavaScript code used.This code is included in boilerplate, there is no need to copy it.https://github.com/adobe/helix-project-boilerplate/blob/27e8571592220da8ded7c8a7e5064d982f7cfe76/styles/styles.css#L45-L51

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/block-collection/images

Pictures, Illustrations and Images are an integral part of making a web experience both more informative and also more engaging. Simply add images to your documents either by inserting them or just copy/paste images from your source.
Add Alt-texts to your images to make your content more accessible. Both Word and Google Docs have great support for that.
AEM automatically takes care of resizing the images to the appropriate size for the website and converting them into the formats that make sense for the consuming browser.
We recommend inserting your images with at least a width of 2000 pixels if available, so even at a large display resolution the images will still be crisp.
The first image on a page will automatically become the image that is used in metadata (for social media and messaging applications) unless defined otherwise in metadata.
Special Mention for GDocs: Google downsizes the images to a maximum of 1600 pixels width or 1600 height. This is still sufficient for most use cases of photography images, but for some illustrations it can lead to visible artifacts.
As images are considered Default Content they are styled in project or block CSS code. There is usually no JavaScript code used.This code is included in boilerplate, there is no need to copy it.
View Code

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/block-collection/lists

Lists serve many purposes in the Web in general, some uses or obvious lists inside default content, while others are used in navigations or other hierarchical constructs.
Extraction of nested numbered lists and bullet lists is supported. Lists are converted to the <ol> and <ul> HTML tags respectively.
Complex list items seem to be hard to manage in word processing without accidentally being broken up so it is generally recommended to keep lists relatively simple when it comes to the complexity of the items in the list.
As lists are considered Default Content they are styled in project or block CSS code. There is usually no JavaScript code used.This code is included in Boilerplate, there is no need to copy it.

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/block-collection/links

Hyperlinks are essential to connect websites and your content. To create a link, just use the insert link option in word or google doc.
Links can be added across all the default content and the different formatting options.
In Word and Google Docs only absolute links are accepted, which is usually easier to copy paste from your browser. Links are automatically converted to be relative to your site, while external links are kept absolute.
Links are often used beyond text links and reference for example embedded media or referenced fragments that are inlined in the page.
As links are considered Default Content they are styled in project or block CSS code. There is usually no JavaScript code used.There is no link related styling code in the boilerplate project.
Special Mention: Microsoft Word Online does not allow links on images, so a workaround would be to let authors put a link directly below the image and then wrap it on the client side, e.g.
Special Mention: It is recommended to handle certain links that need to be opened in a new window based on whether they are external links or PDFs (for example) on the client side, e.g.

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/block-collection/buttons

Websites very often contain buttons, as call to actions or more generically. By default in the Boilerplate project buttons are created as a link in a paragraph by itself.
Bold and Italic (<strong> and <em>) and possibly combinations thereof are used to specify certain types of buttons. There are often primary and secondary buttons and the default styling is usually defined for default content and/or specified by a containing block, and Bold and Italic can be used to specify alternative variations of buttons.
As Buttons are considered Default Content they are styled in project or block CSS code.
There is Javascript code for decoration purposes that is included in the default boilerplate behavior and it usually remains unchanged.Decoration CodeThe CSS Styling is very project specific and gets adjusted as needed for a project or block by block.
Styling Code
All the code above is part of the Boilerplate project and does not need to be copied.

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/block-collection/code

Most technical documentation websites (including this one) have the need to display code. Most marketing websites don’t have that requirement, but since there is an intuitive and simple styling and markup the notion of code elements both inline and preformatted multiline is supported out of the box.
Formatting something in word or gdoc as a fixed font (eg. Courier New) will automatically output a <code> element or a <code> and <pre> block for multiline.
As the code functionality is part of Default Content it is usually really just a matter of styling things according to the project specific CSS rules.This code is part of boilerplate so there is no need to copy it.
See Boilerplate Styling

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/block-collection/sections

Sections are the top level grouping mechanism in documents, think of them as containers for a set of default content and blocks. Learn more about the Document Structure here.
Sections are separated by “Horizontal Rules” or --- to group certain elements of a page together. There may be both semantic and design reasons to group content together, a simple case could be that a section of a page has a different background color.
Technically a section just introduces a <div> wrapper in the markup delivered around all the blocks and default content contained in the section.
In most cases generic sections don’t have much styling code beyond project specific box layout (eg. margins, padding, max-width) and are sometimes augmented with section metadata to control styling (often background colors or images).
See Section Styling Code

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/block-collection/icons

Most if not all websites have icons, therefore there is a simple way to reference icons for authors.
Icons are referenced as :<iconname>: notation. As there are different ways to implement icons in the browser either as plain css classes, icon fonts or SVG, we offer a non-intrusive way to support all of those.
The boilerplate project includes an automatic mechanism to insert SVGs into the icon `<span>`s as that’s the most common way to deal with icons.
While some icons need to be in the code (icons referenced in blocks for example), there are times when authors need to add and reference new icons and update them on an ongoing basis. These icons can and should live with the content under an /icons/ folder in the content source (eg. Sharepoint or Google Drive). These icons can also be referenced the exact same way using the :<iconname>: notation. This will allow marketers to add and update icons they need for content without any dependency on a code change.
The :<iconname>: can be inserted as a part of all Default Content constructs.
Icons are Default Content and are styled in project specific CSS code. If there is any JavaScript that is required to load the SVGs it can be adapted as needed.This code is included in Boilerplate, there is no need to copy it.
See SVG loading Code

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/block-collection/header

Header (Block)
The header block is loaded by default in the boilerplate project into the <header> element.Out-of-the-box it provides code for a responsive navigation but it is like most blocks very likely to be extended or adjusted on the per project basis.
The header block is usually not referenced by authors but is loaded automatically on every page.
The content for the navigation is loaded as a fragment and is by default authored in a nav (or nav.docx) document, and structured primarily into three sections for branding, navigation sections (nested <ul>) and tools.
The nav document has its own lifecycle and when previewed or published applies to all pages that use a given navigation.
It is not uncommon to have multiple nav documents for a site eg. one per locale / language or section of a site.
This code is included in Boilerplate, there is no need to copy it.
Boilerplate Block Code

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/block-collection/footer

Footer (Block)
The footer block is loaded by default in the boilerplate project into the <footer> element.Out-of-the-box it provides a simple example for a footer but is likely to be extended or adjusted on the per project basis.
The footer block is usually not referenced by authors but is loaded automatically on every page.
The content for the footer is loaded as a fragment and is by default authored in a footer (or footer.docx) document.As footer structure and designs change rarely and are usually visually very different from the rest of the blocks on a site, it is often a good strategy to divide the content into sections and decorate specific classes onto the sections based on their sequence and apply CSS styling to those classes.
The footer document has its own lifecycle and when previewed or published applies to all pages that use a given navigation.
It is not uncommon to have multiple footer documents for a site eg. one per locale / language.

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/block-collection/metadata

The Metadata table is handled by HTML rendering service internally to add <meta> tags in the <head> of the HTML markup delivered from the service. There should only be one Metadata table per page and while its placement doesn’t matter, by convention it is placed at the bottom of the document.
The metadata table is essentially following an intuitive name/value pair structure where the name is in the first column of the table and the value is in the second column.
There are a few well-known properties that behave according to the HTML specification and popular additional metadata schemes like og: and twitter: the well known metadata properties include Title, Description, Image, Tags and Feed.
Beyond the well-known properties there are some special semantics for Theme and Template that are added as classes to the <body> tag by the boilerplate code and are often used for styling and autoblocking.
Beyond that a project can add an arbitrary number of name value pairs that get added as <meta> tags to the markup, and can be used with project specific semantics.
The metadata table is processed as part of the HTML rendering service. There is no project code related to the processing.

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/block-collection/section-metadata

The Section Metadata table is handled by boilerplate code internally to add data-*s attributes to the containing section.
Section Metadata table follows an intuitive name/value pair structure where the name is in the first column of the table and the value is in the second column.
The Style property is translated into a class while any other name will be transformed into a data-* attribute.
As Section Metadata generally adds complexity for authors, it is recommended to avoid it, until it is really necessary.
The section metadata table is processed as part of the boilerplate code. There is no project code related to the processing.

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/block-collection/embed

Embed (Block)
It is very common on news and blog websites that videos or social media is embedded in a website.The embed block provides a simple blueprint with some of the most popular embeds as examples.It is likely that this block will be extended with specific social media, video or other embeddable widgets according to a website’s specific needs.
While it is tempting to add as many embeddable URLs into the embed block as possible, we will try to keep the embed block in the block collection to very commonly used platforms, and therefore keep the block simple and easy to extend.
This code is included in Block Collection, simply copying the .css file and the .js file will add this block to your project.
Block Code

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/block-collection/breadcrumbs

Breadcrumbs are a list of page titles and relevant links showing the location of the current page in the navigational hierarchy.
This code is included in the header block in AEM Block Collection, simply copying the .css file and the .js file will add this block to your project.

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/block-collection/carousel

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/block-collection/modal

A modal is a popup that appears over other site content. It requires a click interaction from the user to open, and another interaction to close before they can return to the site underneath.
The modal is not a traditional block. Instead, links to a /modals/ path automatically create a modal.
This code is included in AEM Block Collection, simply:
copy the .css file and the .js file and add to your project.
copy the autoLinkModals() function and add to your scripts.js file
Block CodeScripts Code

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/block-collection/search

Search allows users to find site content by entering a search term. If a content source is not provided, the site’s /query-index.json will be used.
This code is included in AEM Block Collection, simply copying the .css file and the .js file will add this block to your project.

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/block-party/

The Block Party is a place for the AEM developer community to showcase what they have built on AEM sites. It also allows others to avoid reinventing the wheel and reuse these blocks / code snippets / integrations built by the community and tweak the code as necessary to fit their own projects.
If you are an AEM Developer and would like to submit your cool block / code snippet or integration, please enter your submission using this form.
Code snippetamol-anand
AEM Edge Delivery blocks as web components
Preview
Easily convert an Edge Delivery block to a web component to be used in other sections of the site that are not on AEM Edge Delivery. This can be useful when you have part of the site on AEM Edge Delivery and another part on a different platform so that you can reuse the same header and footer from AEM across the site.
Blockmiakobchuk
TrustedSite trustmark
"Certified Secure" certification by TrustedSite.com
Display the TrustedSite trustmark in iframe modal
Code snippetalexcarol
Lottie player integration
Shows how to add a lottie-player animation without negatively affecting lighthouse score by leveraging placeholders and delayed.js.
Sidekick pluginshsteimer
References Check
Finds the references used by a page (forms, fragments, links, etc.) and gives authors one-click access to view and edit them. Can also check for incoming references.
Sidekick pluginusman-khalid
Accessibility Mode
This plugin adds a button in the sidekick to enable "Accessibility Mode" on a given Franklin page, which audits the page from a content perspective and reports on things like missing alt text on images, ease of readability and other accessibility items.
It is also extendable to create custom checks, which can be used to promote correct block usage and adhering to style guidelines, for example. There is an example of this in the repo which checks for too many instances of a block on a page and reports on it.
It can be used by content authors to view the page and address any content or accessibility related issues as a preflight check before publishing.
Otheramol-anand
Log Viewer
Easy way to view logs for your AEM Edge Delivery project
OtherVISUAL-COMPARE.md
Visual Comparison Tool for webpages
This is a visual comparison tool for comparing webpages. Oftentimes while working on styling changes for a site, we want to assess the impact of that change throughout the site. Doing this manually for all the pages is a tedious process. With this tool, this task is automated to a considerable extent.
Once the branch reference and the list of URLs is provided to it for visually testing, it goes through all the URLs on the "main" and "test" branches and loads them in a Playwright based headless browser, takes screenshots and records the differences.
The tool informs the user about the differences and provides location of the screenshots where the differences can be reviewed.
Othersynox
Replace links in all .docx files using Excel sheet
Recursively replaces outdated links in Word documents with new links based on an Excel sheet with the mapping, usually the redirects.xlsx.
Code snippetsdmcraft
External Images
This code snippet demonstrates a mechanism for using images on an AEM AEM web page that are fetched from an external system (outside of AEM AEM)
Othervtsaplin
AEM Expressions
AEM Expressions allow users to transform AEM documents into templates by adding simple expressions with parameters. These expressions then become HTML elements that display dynamically fetched content. It is also possible to use expressions as decorators to style and augment content around the insertion point. This turns expressions into reusable fragments that can be placed inside top level AEM blocks.
Code snippetramboz
Creating Icon Sprite
A mechanism to inline SVG icons directly in the document so that the icons can directly be styled from the CSS (size, color, etc.)
RSS Feed generator Github Workflow
This Github Workflow updates the RSS feed XML every time a new page is published.
Workflow: Link
Scripts: Link
Otherbosschaert
docxtools: work with multiple .docx files from the command-line linux-style
This command-line tool allows you to perform tasks on a directory tree of .docx files a bit like you could with the linux 'grep' or 'sed' commands. Currently supported are:
* cat - output text content of docx file to console
* grep - search for a regex in the document text
* replace/replace-link search and replace text or hyperlinks inside the word .docx files
Note: the tool is written in Rust and the releases provide platform-specific executables. If you need a different platform ping the David B or contribute a PR.
Sidekick pluginherzog31
Block and Section Scheduling
This code lets you schedule blocks or sections. Simply add a date or date range as row to your block or section metadata table and your content will only be displayed after the date or within the range.
This comes with a Sidekick extension, which allows to you preview past or future content.
Not suitable for confidential data, since the scheduled content will still be in the DOM.
Code snippetandreituicu
DOM Helpers (React JSX-Like)
Dom Helpers inspired by React JSX to make the AEM JS code more concise, easier to write, understand and review. DOM like syntax structure to easily visualise the resulting HTML when looking at the code using 100% vanilla JS with no compilation or external dependencies required. Minimal overhead (a few ifs and function calls) allowing you to maintain 100 LHS and avoid the pitfalls of using string templates to DOM manipulation. Can be used both for rendering dynamic data from sheets or with data coming from word documents.
Blockshsteimer
Breadcrumb
Creates a breadcrumb nav based on page titles of parent pages
Blockdave-fink
Image-Compare
This is a simple content block to compare two images with a slider to show or hide the left or right image.
Blockniekraaijmakers
Tabs block based on sections that are auto-blocked in scripts.js
Supports "nesting" blocks in the tab block as well as section metadata such as styles.
Tab block logic controlled partially by css
Blockjalagari
Form Block
Form Block with various capabilities
- Google Recaptcha Integration
- Attachment Support in Forms.
- Post Processing
- Email Notification
- Sync Data to Marketo or Salesforce.
Code snippetfkakatie
Images with links
This snippet adds ihyperlinks to images.
In order to use it, refer the linked code snippet. And while authoring in Wod/GDoc, specify the link url immediately after the image.
Code snippetshsteimer
Dynamically loaded Templates
Load CSS and JS specific to a template, allowing for template specific styling and auto-blocking, without intermingling that code into global scripts/styles.
Note: because the template js is loaded before blocks are loaded, but after sections/blocks are decorated, auto blocking needs to be done with that in mind (that is, build and decorate your blocks, and add them to a section, but do not load them).
Code snippetBuuhuu
ffetch
ffetch is a small wrapper around the JavaScript fetch function that helps you deal with the AEM Content API when building a composable application. It makes it easy to fetch content from a AEM Index, apply lazy pagination, follow links to pages, and even pull page metadata. With ffetch you get all the ease of creating a headless application without the peformance baggage of headless SDKs and the complexity of headless APIs.
Build toolingsachioross
SASS + AEM
SASS compile for your CSS while locally developing in AEM
Code snippetdavidnuescheler
Metatag / JSON based CSP
This approach is setting a CSP in a way that is only transported over the wire once and then cached on the client.
Allows to manage the CSP in an easy to read JSON file
Sidekick plugindylandepass
Sidekick Library
The Sidekick Library is an extension for the AEM Sidekick that enables developers to create UI-driven tooling for content authors. It includes a built-in blocks plugin that can display a list of all blocks to authors in an intuitive manner, removing the need for authors to remember or search for every variation of a block. Developers can also write their own plugins for the sidekick library.
Code snippetmsagolj
Video autoplay
To autoplay videos across different browsers, a set of attributes need to be set.
It is important to set videoTag.muted = true exactly this way otherwise it won't play in chrome. Eg. setAttribute('muted', true) doesn't work.
Code snippethannessolo
Script to add modals to your franklin page. This uses the new(ish) native element.
You can see it in action here: Link

*** End of Page ***

*** New Page ***
https://www.aem.live/developer/block-party/submission

AEM Block Party
With Block Party, we would like to give our passionate developer community a place to showcase what they have built on AEM sites. It also allows others to avoid reinventing the wheel and reuse these blocks / code snippets / integrations built by the community and tweak the code as necessary to fit their own projects.
If you are an AEM developer and would like to submit your cool block / code snippet or integration, please enter your submission in the form below:

*** End of Page ***

This part of the instructions is intended to train an ai in the coding style for an adobe product. The product uses CSS3, modern javascript and DOM manipulation instead of libraries

Instructions

The following blocks of text is data scraped from an instructional website

*** New Page ***
https://allabout.network/blogs/ddt/developer-guide-to-document-authoring-with-edge-delivery-services-part-0

Developer Guide to Document Authoring with Edge Delivery Services. Part 0
Introduction to Document Authoring with Edge Delivery Services
I discuss how Edge Delivery Services works with providers, focusing on Document Authoring with Edge Delivery Services.I create a Hello World block.
Back to Top
Introduction
I am a developer; arrays start at element 0, or I began writing with part 1 and now need an introduction. You choose.
This series of articles discusses developing with Adobe’s Edge Delivery Services in the Context of Document Authoring.
What is Document Authoring?
Creating and publishing content in the digital landscape involves multiple steps and stakeholders. Subject Matter Experts generate original content, which goes through an approval process. The approved content is then passed to Content Authors, who copy and paste sub-edited content into the Content Management System (CMS) for publishing. This process can be time-consuming, especially in regulated industries, where the approval process can take a long time.
Content Authors dislike CMS systems. They are forced to use “components” and “templates” created by technicians, using “page properties” and “dialog boxes” to pull the whole piece together; they have to sub-edit the copy because it often won’t fit in the constraints of the CMS. They usually have to backport the content to the original document format as a system of record. Returning the document to the SME with a link to the final web-page page.
Product Owners dislike CMS systems, especially in regulated industries. They must get approval for every line of text they place on the web. Regulated industries, like pharmaceuticals, automobile makers, insurers, etc., are legally bound by the content they publish and the corporation's stringent requirements. One product owner I know of asked that pre-approved Excel spreadsheets drive the entire CMS; however, the technicians have built the components and templates around dialogue boxes. He, too, had to use the copy-paste methodology.
Enter Document Authoring with Edge Delivery Services from Adobe, sometimes called EDS. This system stands out for its innovative approach to web publishing and promises to redefine efficiency and simplicity in the industry.
It is a cutting-edge document-based CMS, named in tribute to Rosalind Franklin, whose work was crucial in deciphering DNA's structure. It has transformed from the "Helix" concept to the current "Edge Delivery Services," signifying its growth from a rudimentary tool to a comprehensive digital content management solution. It has shared all three names in its lifetime: Franklin, Helix and now Edge Delivery Services.
Why Document Authoring with Adobe Edge Delivery Services Matters
Edge Delivery Services' significance lies in its speed and serverless architecture, which eliminates the complexities of server management and offers a seamless experience for content creators. It integrates effortlessly with tools that users are already familiar with, making content editing and management as intuitive as using everyday office applications. It offers a rapid development and release environment for document-based content creation, keeping the Content Authors and Product Managers happy and using tools they know well.
Key features:
Improved speed, scalability and authoring experience
Reduced coding requirements
Streamlined content creation and delivery
Quicker website updates
Benefits:
Enhanced user experience
Increased efficiency
Reduced costs
Use cases:
Building high-performance websites
Landing page creation
Delivering rich media content
Creating personalized digital experiences
Conclusion:Edge Delivery Services is a powerful tool for businesses looking to enhance their online presence. Its focus on speed, scalability and ease of use can help companies to create engaging and compelling digital experiences.
Weighing the Benefits and Limitations
Edge Delivery Services is designed to deliver content with unparalleled efficiency, addressing the challenges of handling extensive information without compromising performance. It opens the content management field to a broader audience, enabling more professionals to contribute effectively. However, it's essential to acknowledge that Document Authoring with Edge Delivery Services may not be the one-size-fits-all solution for every scenario, mainly where intricate customization and deep content modelling and structuring are paramount. Edge Delivery Services offers deeper integration with traditional AEM for requests that require content modelling and deeper integration with the back-end. Adobe has you covered. Think of Document Authoring with Edge Delivery Services as the rapid delivery of traditional documents - ‘a Content Marketing System’. When customers require more sophisticated back-ends, AEM Authoring with Edge Delivery Services is ready to take up the workload and continue to integrate with the document authoring solutions in place.
Is it Edge Delivery Service or Edge Delivery Services?
Good question. It is Edge Delivery Services, as many different services are involved in delivery at the Edge (close to the customer). There are asset services, authentication services, JSON services, content services, and CDN (Content Delivery Network) services. The Adobe-provided services run on ADOBE IO, a serverless environment.
Edge Delivery Services is the final publishing point; many projects provide content for this platform.
At the time of writing, there are three providers (in order of appearance):
gdocs2md (Google)
word2md (Microsoft)
html2md (Bring your own markup)
The last one supports:
Crosswalk, Commerce, and Dark Alley. Universal Editor is something that is sitting on top of Crosswalk (from the perspective of Edge Delivery Services)
Project Helix (which was later extended into Franklin) covers delivery, authoring (through Sidekick), and integration (through the Admin API). To refer to the Word and Google integration only as Helix isn't how the Adobe team has been looking at it - and still isn't, as they use Helix now to refer to the same core architecture.
Edge Delivery Services (for AEM Sites) includes only the delivery part, as far as the product is packaged. There is no official name for the whole stack.
This is a complex relationship, making discussion challenging. I will focus on Document Authoring with Edge Delivery Services.
Each provider creates digital entities that are stored in a content bus. This includes content from cloud-based sources, including code, assets, text, HTML, JSON and configuration data. The platform serves the content bus.
It is essential to keep these two entities separate in your mind. There are providers, and Edge Delivery Services is the platform.
Why invest in this technology?
Performance. Edge Delivery Services is faster, which increases customer engagement. There are lots of studies showing that speed counts. Adobe wants to create the world's fastest websites, so it experimented to see what would happen if all our customers had the world's fastest sites! So Adobe did this.
Surely a CDN does this for AEM?
It helps, but there is more than just CDN delivery. CDNs are great for global audiences, but users usually have multiple CDNs, dispatcher caches, Akamai, etc. Edge Delivery Services is not a CDN. It sits side by side, feeding one CDN! It runs next to your CDN, not in parallel.
What speed do we expect?
Lighthouse has its challenges. Edge Delivery Services sends pages optimally, not including any JS cumulative layout shift. If you get 100 on Lighthouse, you will be among the fastest in the world. Adobe aimed for 80-100 for Edge Delivery Services, and they achieved it.
Is this just for static content?
Great question. The magic is caused by rethinking resources, with a server component that organises the resources and a client part that personalises. Working together, these two parts allow super-fast dynamic page content. Adobe first focused on what was visible to the user, improving static and dynamic pages. There is no such thing as a static page in Edge Delivery Services.
Did Adobe create a new JS framework to do this?
Current front-end frameworks are designed around limitations in the stack, but browsers have improved, protocols have changed, HTTP/3 is available, and limitations are gone. So Adobe does not call what they are doing a “framework”; it is the web platform. Adobe uses the best bits from the new world. In the old world, software used a bundler to consume assets and placed it on a server for a client to consume. Edge Delivery Services is a no-bundler; source code coordination is used instead. The goal was to make Edge Delivery Services easy to use and make the time to rebuild a site super fast. Instead of incrementally adapting a site to a new design, authors use the tools they have learnt: Excel, Word, Google Drive, etc.
What does the platform bring?
Performance optimisation: Edge Delivery Services focuses heavily on web performance. It helps serve websites at lightning-fast speeds from edge locations (geographically closer to users) for lower loading times. Unmatched speed – get a Lighthouse reading of 100s.
Content at scale: It can handle thousands of pages. Adobe has 300,000+ pages on its website, as Edge Delivery Services pages.
Content first: Whether structured or unstructured, AEM prioritises your content, delivers it optimally, and provides your developers with everything they need to make it shine.
Enhanced developer experience: Edge Delivery Services empowers developers using modern front-end technologies and frameworks..
Real user monitoring (RUM) provides real-time performance metrics, so any dips or issues can be addressed quickly, maintaining a great user experience.
Flexibility: Edge Delivery Services is composable, meaning you can use only the specific services you need for your project. No matter where your data is, you can bring it into Edge Delivery Services.
Why Edge Delivery Services Matters
Website speed and user experience are crucial. Edge Delivery Services helps businesses achieve:
Faster time to market: Get content updates live much quicker. With its serverless, lightweight approach to web development, Edge Delivery Services allows you to go live in weeks, onboard new developers in days and iterate on changes in minutes. Unlimited preview sites give you confidence that changes will be what you need.
Improved SEO: Faster websites get a boost in search rankings.
Increased customer engagement: A smooth, fast website keeps users engaged, leading to potentially higher conversion rates.
Zero backend development: No building via Maven, Grunt, etc. Edge Delivery Services uses everything modern web developers love: GitHub, local development with auto-reload, performance, and simplicity. And none of the complications: no transpilation, no bundlers, no configurations, no overhead, no pipelines.
Low-code productivity: No frameworks, plain HTML, modern CSS and vanilla JavaScript. Simple to onboard devs: no backend (needs repeating!).
One less thing to be on call for. Incredible availability due to fully redundant architecture, endless scalability as a full software-as-a-service, and easy integrations with your existing CDN make go-live anxiety a thing of the past.
From Cedric Heusler: The short version is that we are building the “bottomless CMS” - a set of CMS services independent of the content. The content is either already in the cloud (SharePoint, etc.) or managed with a domain-specific workflow AND in a CMS. All this content comes together in the brand/enterprise experience.
How does Edge Delivery Services differ from AEM?
Unlike traditional content management systems (CMSs), this solution does not require a dedicated authoring or publishing environment. Instead, it leverages platforms like Google Drive or SharePoint to create and manage content published directly to Edge Delivery Services for fast and reliable delivery to end-users. This serverless approach eliminates the need for complex infrastructure and maintenance, making it a cost-effective and scalable option for content delivery.
Components can be used immediately as the pages have been built beforehand, eliminating the need to wait for a CDN call to the dispatcher or for the components to run on a publisher.
No bundling of JS/CSS (Clientlibs).
The code relies solely on modern JavaScript and CSS without needing external JavaScript libraries such as jQuery.
Pages are controlled through spreadsheets, redirects, indexes, and metadata, all converted into JSON by Edge Delivery Services.
Paths in Google Drive/SharePoint control the tree structure of pages.
Plug-in tools like Asset Picker from Adobe and Translate from Google can be used.
Developers store code in GitHub: no build to deploy.
Each branch in GitHub has a separate live preview.
Zero pipelines, no delay in publishing. Fast.
Did I say fast and easy using tools you already know?
To end this introduction, I must create a ‘hello world’ for Document Authoring with Edge Delivery Services.
Code is coordinated by scripts.js in the scripts folder.
You can see the code in scripts that control loading:
This pattern is known as E-L-D, Eager, Lazy, Delayed, which optimizes the page loading sequence to help us get to the 100/100/100/100 model.
Earlier in the sequence, scripts.js inspects the page held in the content bus and modified to be the intermediate state; the code scans for ‘blocks’ and when finding one, it injects the cs and javascript, executing the JS if any is placed in line.
Creating HelloWorld
I will assume that you have cloned the https://github.com/adobe/aem-boilerplate repo and setup your Edge Delivery Services folder following Getting Started – Developer Tutorial (www.aem.live)
To create a simple "Hello World" block in Edge Delivery Services, you'll need to create a CSS file for styling and a JavaScript file for functionality. Here's how you can do it:
Create a new folder named "helloworld" in your blocks directory in the GitHub repository
Create two files inside the "helloworld" folder: helloworld.css and helloworld.js.
In helloworld.css, add the following content:
In helloworld.js, add the following content:
In your Google document, insert a table, one cell wide and one cell deep.
In this cell, add the text ‘helloworld’
The existence of the table with the first cell name is the link to the code; Edge Delivery Services looks in the blocks folder and cell-name folder for similarly named js and css, applying the js and the css to the DOM. Magic completed, and the results look like this.
Keep reading the series to find out more
Related Articles
Return to Top

*** End of Page ***

*** New Page ***
https://allabout.network/blogs/ddt/developer-guide-to-document-authoring-with-edge-delivery-services-part-1

Developer Guide to Document Authoring with Edge Delivery Services. Part 1
This tutorial will guide you through creating your blocks and amending the HTML provided by Edge Delivery Services to ensure that the page has the look and feel you need, with coding examples.
Simple web page, served vs rendered
Hero block
Hero block with mobile variation
This is a developer's guide to Document Authoring with Edge Delivery Services. It assumes you have access to a content team to create the content.
I was only joking. Developers can type content.
Document-based authoring: Edge Delivery Services allows content creators to use familiar tools like Microsoft Word, Google Docs and spreadsheets to author and structure website content, greatly simplifying the process.
All work on a modern website starts with a product owner, a creative person who wants to describe a product or a service. A marketing lead often briefs the product owner. The initial stages usually start with a document, such as Word or Google Docs. The idea is fleshed out and spread around the various teams until everybody is happy with the outcome, messaging and possibly pricing. Sample images and other content are often embedded in the document.
This is then sent to the web content team, which will design a new web page in the site hierarchy or a landing page distinct from the rest of the website. Based on the document's contents, the team may have a design ready or commission one.
The team then copies and pastes the images and text from the document into ‘components’ placed on a ‘template’ within the CMS. Sometimes, they need to adjust the copy to fit the page template or component. When changes have been made, the copied and pasted text is placed back into the document as a system of record, and when the web page has been published, the amended document is sent back to the product owner.
This can be a long process. Perhaps templates and components need building from a busy dev team; the content team is busy creating the next season's campaigns; the author server is down for routine maintenance. The page may not need much design; it may be design-ready with known components, and you might have Franklin (Document Authoring with Edge Delivery Services). Good. We will publish the page and step through the processes, adjusting code and styling.
If you do not have Document Authoring with Edge Delivery Services installed, the tutorial at https://aem.live is easy to follow.
Beginning with a simple page,
We begin with a simple page, one with a simple title and simple text in Google Docs:
The document above is named Hello World, and the text states, “Hello World From Edge Delivery Services.” Pressing the Preview button transfers the document as HTML to Edge Delivery Services’ content bus, which stores all the content for the eventual web pages. We also have a code bus and an asset bus.
The system then creates a web page.
Not very complicated. Look at the ‘view page source’ code:
What happened?
The title, og:title and twitter:title were created from the first and only line in the text, and the canonical meta tag and the og:url were created from the page's location, formed by domain name/Google Drive location.
Also note that the domain name, in this case main--allaboutv2--ddttom was taken from my GitHub branch, repo name and username. If I created a new branch, say testing, then the domain name would change to testing--allaboutv2--ddttom. The double hyphens -- separate each element. This allows for the testing of new code without breaking the site.
A default image tag for ogsecure_url, og:image and twitter:image has been provided. I must provide this default image in my codebase.
The viewport tag has been provided.
Finally, the scripts aem.js and scripts.js are loaded
I have an empty header and footer in my Franklin implementations. If one wants these to be present, one can create them in Google Drive, following similar rules to page creation.
The main part of the HTML is a div, followed by a paragraph and text. This is a very simple web page.
But what happens inside the browser?
You will see that the DOM has been adjusted or decorated. This is by design. Franklin provides the document in a raw HTML format with known patterns. It is up to the developers to shape the HTML to the end requirements.
Franklin has added the HTML to load our CSS through style sheets. Two different patterns are used: styles.css and lazy-style.css. As the document is processed, there are three phases: load-eager, load-page, and load-lazy. Load-eager gets the important “before the fold” stuff and everything needed to get the page set up as quickly as possible; load-page is our normal run-of-the-mill stuff, and load-lazy is for actions that don’t matter, such as animations. Further details will be provided later.
Franklin has injected extra scripts into the header, inspecting the raw HTML and deciding which components are used. In our case, we have a header and a footer (though both are blank). Franklin has decided that we are using the header and footer components. Franklin calls the components ‘blocks,’ so it adds the CSS to the DOM and runs the JS from the same folder.
.
We are using modern JavaScript techniques in Franklin, so the JavaScript is dynamically imported, similar to:
Although it uses a more elegant method.
The JavaScript that comes with the blocks can modify and shape the DOM to match the styling requirements. This is a more advanced technique that will be explained later.
The standard “out of the box” causes the body element to be non-displayed.
You should also note that we are using CSS3 variables in our CSS.
When the page is finally ready to be seen, the class “appear” is applied to the body, and the page becomes visible, thus stopping any flashes of unstyled content.
Semantic header and footer elements are added to the DOM, and wrappers are added as a class to each part. The document is split into sections, only one in this raw case. As the JS runs for each ‘block’ or ‘section’, a data-block-status or a section-block-status class with a value of “loaded” is added; this will help prevent this fraction of the DOM from being handled more than once. It's also a signal to the CSS to action new styles.
The raw line of text is given a <div class=”default-content-wrapper”>
In summary, Franklin applies clever conventions on top of the Google/Word Doc, sending minimal HTML to the browser with minimal JavaScript, preferably over an HTTP/3 channel. The JavaScript inspects the HTML and then decorates it by following rules.
We have yet to discover the rules in this document. As there is no configuration in the document, Franklin uses a technique known as auto-blocking, which uses ‘smart algorithms’ to understand the author's intent.
What’s this about rules?
Documents need descriptions, titles, etc. Franklin tries to provide them, but it would be better if the author had control. In document authoring, we use tables to give rules.
The image above shows our document with a table with two columns. The first row is merged and given the name ‘metadata’. This is the clue to Franklin to use this information to adjust the metadata in the HTML.
The left column contains the key phrase, and the right column contains the content. It is traditional to place this table at the foot of the document.
What this metadata does to our HTML
In this HTML, you can see that the title matches our title and that the description is mapped to the description fields in the og:description and twitter:description fields.
A new meta field has been added:
<meta name="keywords" content="Explanatory, web, page, hello, world">
This ability to add rules to the document is key to creating interesting web pages.
We can add one of the built-in rules. If they contain code and styling, they are called blocks.
Add a block called columns:
Now inspect the view page source. We will only look at <main> as we have not changed the metadata.
The block name (columns) is now held within a div element with a class of “columns.” This signals Franklin to adjust the DOM, as we will see later.
Although we used a table to define our block, Franklin used div elements to separate each cell. We have not gone back in time and are about to style tables. Franklin is much smarter than that.
Inspecting the rendered DOM, where .…. represents unchanged parts
The image in og:image and ogsecure_url has been replaced from the default with the URL for the image in the metadata table.
This is our first time using an image in our sample document. You will notice that the doc format has embedded the image into the document, and its original name is lost. Franklin creates a unique ID. If the same image is used in multiple places, the ID is the same; it is a hash of the image. You will notice in the HTML a query string: ?width=1200&#x26;format=pjpg&#x26;optimize=medium"
The &#x26; is an HTML entity representing the ampersand character (&). It is inserted in the string to disambiguate the parameters. So, it states that the image should be resized to 1200 pixels in width and served as a progressive jpeg with medium optimisation. Noting this, it is best to provide an image 1200 pixels wide in the image metadata portion of the page. Franklin will stretch or shrink your image to fit if you do not.
We will add images to our simple website and see what Franklin offers later.
The CSS for columns has been added to the DOM:
At the same time as this decoration, the js in /blocks/columns/columns.js has been imported and executed.
A div with class=”columns-wrapper” has been added to the DOM just before the text.
The original div class = ”columns” has been extended, and new classes have been added: block to inform Franklin that this is a block and columns-2-cols to ensure that the correct styling for a two-column div is applied.
The code in column.js does this decoration to transform the raw HTML into a structure that can be styled.
The code above reads the number of divs in the first child and gets the count; it then adds the class with a count derived from the number of children.
The last part of the js looks for picture elements - added at the document level - and decorates the cell with columns-img-col if found.
Adjusting our document and placing the same image at the top of the page, with author details.
I have opened the image options panel in Google Drive to show the placement of alt text and title of this image. I am using the same image as I used in the metadata; we will be able to see the different treatments given to the image by Edge Delivery Services.
Preview the page in the browser.
Inspecting the view page source, from main onwards:
Keep in mind the original og:image metadata:
<meta property="og:image" content="https://main--allaboutv2--ddttom.hlx.page/media_145e13ea388af99109b4e34d2c57d40f5fc22d9c9.jpeg?width=1200&#x26;format=pjpg&#x26;optimize=medium">
The HTML provided shows that the picture element was used. The HTML picture element is a powerful tool for creating websites. It allows web developers to specify multiple image sources, ensuring the browser displays the most appropriate image for the user's device and network conditions. Additionally, the picture element provides a fallback mechanism for browsers not supporting it, ensuring users always see some form of the image.
The picture element constructed has the following pattern. I have abstracted the name and shortened the optimisation part to show the layout.
This element has media query conditions that choose the correct width for a mobile device (min-width:600). There are format options to choose WebP (format=webply) or JPEG (format=jpeg). The browser will use this mix to select the correct image for the viewport size and browser capabilities. Finally, there is a fallback - for browsers or tools that need help understanding the picture element (few these days).
Inspecting the HTML code reveals that the same image is used everywhere as the source. The query-string modifier changes what is served to the browser. In our case:./media_145e13ea388af99109b4e34d2c57d40f5fc22d9c9.jpeg
Creating a bio block
We will improve the content team's experience by creating a bio block and writing some JavaScript and CSS to add the image title as a byline for the bio. Search for the meta author if the image does not contain a title.
<meta name="author" content="Tom Cranstoun">
The first step is to extend the page metadata table to add a new entry: author. Remember, the first column is case-insensitive, but the second is not.
The next step is encapsulating the content in a new bio block and adding the text underneath.
Now, we must style the block and write the JavaScript to decorate it however we want.
Inspect the new block “bio” when looking at the view source page for the raw HTML:
We know that Edge Delivery Services decorates our block when it downloads; looking at the rendered DOM, just the bio block:
We see that the <div class=”bio”> has been replaced with<div class="bio-wrapper">
<div class="bio block" data-block-name="bio" data-block-status="loaded">
This allows us to target CSS onto the block.
We will put the bio picture into a circle.
The CSS styling is stored in /blocks/bio/bio.css
Developer Guide to document authoring with Edge Delivery Services
We now want to add some JavaScript to retrieve the alt text or, if not present, read the meta name=author content into a Javascript variable named author, using the variable to enhance the bio element display. The content team no longer need to write the author's name if the metadata is correct, either on the alt or the meta author tag.
Create the new \blocks\bio\bio.js
Previewing the page:
By adding classes, we can create variations of our block. We add extra names in brackets after the block's name. We now create a highlighted variation of the bio block.
We adjust our styling in \blocks\bio\bio.css
As we have the circle (border-radius….), the browser version now shows the image with a 2px blue border.
The ability to create many block variations is a powerful feature that can add visual interest and interactivity to a web page. Using styling and JavaScript introspection, unique and engaging blocks can be created.
Here are some examples of how styling and JavaScript introspection can be used to create block variations:
Styling:
Use different colours, fonts and sizes to create various visual styles.
Add borders, shadows and other effects to create depth and dimension.
Use CSS animations and transitions to create dynamic effects.
JavaScript introspection:
Use JavaScript to get and set the properties of a block.
Use JavaScript to add and remove event listeners to a block.
Use JavaScript to create custom animations and interactions.
Here are some additional tips for creating variations of a block:
Use a consistent design system:
This will help ensure your blocks have a cohesive look and feel.
Start with a simple design:
This will make it easier to add variations later.
Use modular components:
This will make it easier to reuse code and create new variations.
Test your variations:
This will help to ensure that your variations work as expected.
By combining styling and JavaScript introspection, blocks can be created that are visually appealing and interactive, creating a more engaging and enjoyable user experience.
Creating a hero block
A hero block, frequently called the hero image or hero banner, is crucial at the start of many web pages. This large, page-wide image is strategically positioned to capture the visitor's attention upon landing immediately.
We begin by inserting a table in the document. The first cell is named Hero, and the image is in the second cell.
When we preview this, the hero takes up the entire screen width.
Even when viewed on a mobile device, it looks good.
You may remember that Edge Delivery Services makes a fresh version at different resolutions (picture element, media-query), transforming JPEG into WebP format. This may distort the mobile view by crushing the pixels, giving you a fuzzy appearance.
You may want a different hero on a mobile, which is an editorial decision. The auto-scaled version of the desktop hero sometimes needs to be fixed for a mobile hero.
How do we do this? With a slight modification of the hero table and a bit of JavaScript.
Add a suitable mobile image in a two-cell wide table.
When this is previewed, we are shocked. It is not what we wanted. We ended up with the second image as the hero; the desktop image got lost.
Looking at the rendered HTML:
Add code to the hero block.
As expected, we see two picture elements. We need to decorate this part of the site to combine them. By convention, the first row is the desktop image, and the second is the mobile image.
We add code to the empty \blocks\hero\hero.js
The code performs manipulations on the picture elements. It starts by selecting two <picture> elements: the first one is located inside the first <div> element within an element with the class name hero, and the second one is located inside the second <div> element within the first <div> element of the hero class.
If both these <picture> elements exist, the code proceeds to do the following:
1. It selects the second <source> element from the second <picture> element.
2. If the second <source> element exists, it creates a copy of it.
3. It then checks if the first <picture> element has a second <source> element.
If it does, the code replaces the existing second <source> element in the first <picture> element with the copied <source> element from the second <picture> element.
If it doesn't, the code adds the copied <source> element as a new child of the first <picture> element.
4. Finally, the code removes the document's second <picture> element.
If only one picture element exists, the code does nothing, and the hero still works.
Just a little touch of magic.
See the screenshots below:
Desktop image
Mobile image
Finally, the rendered picture elements - following the adjustments:
The two picture elements have been merged into one.
All this work has been done without affecting the Lighthouse page speed.
If you follow along, you will have a very similar page. See here An explanatory web page

*** End of Page ***

*** New Page ***
https://allabout.network/blogs/ddt/developer-guide-to-document-authoring-with-edge-delivery-services-part-2

Developer Guide to Document Authoring with Edge Delivery Services. Part 2
Creating variations of our block
Automating Author Name from Alt Text
Place text to the right of BIO
Use of fragments
Theming
This post continues my Developer’s guide to Document Authoring with Edge Delivery Services.
Previously
We created a Google Doc called ‘Hello World’. It has a hero and a bio block, with the line of text “Hello World From Edge Delivery Services.”
We added a metadata table at the bottom of the document, and the entries became metadata on the final web page.
We added JavaScript to the hero block to allow the hero to contain two distinct images: one for mobile and one for desktop. The picture element selected these depending on the viewport size. We created the bio block from scratch, with styling to create a circular overlay and to pick up the author's name from either the alt tag in the img element and, if not present, obtain the author’s name from the author meta element from the page. We added a class name ‘highlighted’ to our Bio block and the CSS to put an outline on the image.
Continuing the journey
Our manager has asked that we follow the image with some text with brief biographical details, including the author's name. Thus, we no longer need the author's name automatically provided underneath the picture. This is another variation.
The cell in the block name is currently Bio (highlighted). The word bio is the block name and also the class name used to create the element and its wrappers; looking at the rendered HTML in the browser after Edge Delivery Services has run, one of the first things to note is that the blocks we created have been rendered in a div with a section class.
<div class="section hero-container bio-container columns-container" data-section-status="loaded" style="">
The div also has classes for each block in the document, postfixed with -container. There is a data-section-status class that is adjusted by edge delivery and, finally, an empty style; we will return to section styles later. Further down, we see the bio-block
In the snippet of HTML, the block is decorated with a -wrapper <div class="bio-wrapper">, and the next div is decorated with the block name ‘bio’ and the variation ‘highlighted’ along with a ‘block’ class.
<div class="bio highlighted block" data-block-name="bio" data-block-status="loaded">
All blocks are decorated this way: block name, variations applied and then ‘block’. This makes it easy to target slices of the HTML using classes; Edge Delivery Services does not use IDs.
Hiding the auto-generated author-name
Our task is to hide the author, so adding a new class name, “hide author”, is done by adding a second phrase after a comma within the brackets. All spaces in the block/styling phrase are replaced with hyphens (-)
Investigating the rendered HTML.
We can adjust the CSS or JS to make this class work. I chose to adjust the javascript, adding a test for the non-existence of the class ‘hide-author.’
The final Javascript function
Place the text to the right.
The next task was to allow text to the right of our bio picture. Insert a new column, merge the title and add the text.
Previewing could be better; the text is underneath.
It looks OK, but we wanted something else. We want the text to be to the right. Adding CSS to bio.css to involve a flex layout does the trick.
We adjusted the first and last child of the DIVs to make it work
Reusing content
Content Management Systems often reuse pieces of text and assets; Edge Delivery Services include this functionality; these are called fragments.
I create fragments in a folder called fragments in my Google Drive. We want boilerplate text at the foot of our pages, similar to the ‘Want to know more’ at the bottom of these articles.
We create the fragment.
And add the content
We need to include this fragment in our page by adding a fragment block with the path of the fragment.
When previewed, we can now see the fragment on our page.
The ‘want to know more’ fragment is displayed on the page; it is interesting to see how Edge Delivery Services provides this snippet; we inspect the network traffic.
This time, you can see the fetch/xhr content in my filter, and the fragment component asked for want-to-know-more-plain.html instead of Edge Delivery providing a page with styling, javascript, <body>, <main>, etc.; it just provided the raw HTML in a div ready for adding to the page; where it will inherit the styling and context of the page.
Looking more carefully, you wiil also see footer.plain.html and nav.plain.html
These are ‘auto fragments’ generated from the footer and nav documents placed in the root folder of Google Drive. This gives the website a consistent set of headers and footers.
I want my allabout.network to be focussed on telling people ‘all about’ something, such as this tutorial, without third-party distractions. Therefore, I have also chosen not to have footers or navigation; you can easily create your navigation and footers and have them auto-included as fragments.
Styling the pages
There are three ways to style your pages.
Use the default autoblock style.
Change nothing; get the style verbatim from styles.css. You can view and change the styles in styles.css for your project. Note that the style sheets in Edge Delivery use CSS 3 with variables; it does not require bundling, compiling, or transpilation.
Use a theme
After applying the theme's name to your metadata table, Edge Delivery Services will apply the theme class to the body. Then, you can change the styling for this page using the theme as a modifier; for instance, a newsletter page could have a theme of ‘newsletter’; as I am writing an article, I will give this page a theme of ‘article’.
Edge Delivery Services applies the theme class to the body element.
One can use different styles on this page, such as margin padding.
Note that the body class has a display: none; when Edge Delivery finishes loading the page, it changes the display attribute by adding a class “appear” with a display “block”
Use sections
Sections are parts of the page; they are marked by ‘---’ 3 hyphens on a line by themselves and have a section metadata table.
Preview the page
The rendered HTML for a section looks like this:
The description metadata is used as a data-description attribute in the section div. You can use these data descriptions to decorate the HTML further, but this will be custom code.
The Hello World page is here.https://allabout.network/blogs/ddt/hello-world-2
A Manager’s Guide to Document Authoring with Edge Delivery Services

*** End of Page ***

*** New Page ***
https://allabout.network/blogs/ddt/a-managers-guide-to-document-authoring-with-edge-delivery-services

This is a manager's guide to Document Authoring with Edge Delivery Services. Although Edge Delivery Services is a serverless platform provided and maintained by Adobe, setting up the web hosting, GitHub, CDN (content delivery network) and DNS (domain name system) management requires some technical skill. This post assumes you have access to a technical team to do this with you.
I cover some of the technical aspects in a developer’s guide to Edge Delivery Services post here:
https://allabout.network/developer-guide-to-document-authoring-with-edge-delivery-services
This guide provides an overview of how Document Authoring with Edge Delivery Services functions. It is designed for managers who require a high-level understanding of the services but do not need to be involved in the developer detail.
Here are the key points to understand:
Purpose: Document Authoring with Edge Delivery Services enables organisations to create, manage and deliver rich, interactive documents with near-instantaneous load times and fast turnaround from concept to go-to-market, with A/B testing.
Improved user experience: Documents load quickly, providing users with a seamless and engaging experience.
Increased productivity: Authors can create and update documents efficiently, as changes are reflected quickly.
Cost savings: Organisations can reduce infrastructure costs by leveraging Edge Delivery Services.
Scalability: The service is highly scalable, ensuring that documents can be delivered to many users without performance issues.
Components:
Authoring environment: Authors use Google or Word documents to create and edit web pages. Spreadsheets to provide configuration.
Edge delivery network: Documents are cached on edge servers strategically located worldwide, ensuring fast delivery to users.
Content delivery network: Documents are delivered to users from the nearest edge server, reducing latency and improving performance.
Technical setup:
Infrastructure: A technical team is required to set up the necessary infrastructure, DNS, GitHub and CDN.
Integration: The service can integrate with existing content management systems and workflows.
Documentation/manuals: Deliver complex technical documentation with interactive diagrams, videos and simulations.
Marketing collateral: Create engaging marketing materials such as brochures, presentations and reports.
Training materials: Develop interactive training modules that can be accessed from anywhere.
Legal documents: Share legal documents with clients and partners securely and efficiently.
Landing pages: Create campaign landing pages, pages for sharing with social media, etc
Everything discussed below is identical if you have an existing Word or Google Doc. I will demonstrate importing an existing web page into the system.
Consider a web page such as
The AI Tipping Point: A Consultant's Takeaways from CMS Kickoff 2024 | CMS Critic
If we want to convert this page to Document Authoring with Edge Delivery Services, we start with the AEM (Adobe Experience Manager) import command. The importer is currently named Franklin Import UI. Franklin is one of the internal product names; another used in the codebase is Helix. Helix refers to the DNA spiral and is used as a meme throughout the documentation and developer “getting started “ site: https://www.aem.live/home
I will continue to user Franklin in this document.
The Edge Delivery Services platform provides the security, scalability, and speed that Adobe offers for websites. Franklin is an individual project that utilizes the Edge Delivery Services content bus.The platform also includes other projects discussed in a separate blog post.
The importer reads the website and recognises the main content, removing the adverts. It then saves the content as a Word document. We edit the document to remove the menu hangover in Word or Google Docs and add a heading title format. That's all we have to do. The document is ready for Edge Delivery Services.
Upload to Google Drive (or SharePoint)
In Google Drive (or SharePoint), we can use the document's normal workflows, plugins, and tools, including translation, AI content enhancement, etc.
When we are ready, press Preview to check the website. Then press Publish, and the web page will be complete and ready for the public.
This technique is called ‘autoblocking’ in the Franklin project. We have not created any components, added fancy tags, or used JavaScript Blocks on the web page. We have taken a basic article with images and turned it into a reasonable web page. Franklin has preserved font emboldening, heading levels, and links and loosely styled them all, which Edge Delivery Services will serve fast.
Franklin understands the document's headings (h1, h2, etc.) and applies these values to the final web page. Franklin also understands bold, italic, and underlining. Franklin understands the Courier font as a code marker and outputs the content raw, without font decoration.
Adding new items
We want to be creative and add new items to our site. Let's explain how that works.
In the GitHub repository, we store individual code and styling in blocks. Your developers take care of these, and your integrator or developer team will provide some additional blocks.
When applying code or styling to a fraction of our web page, one adds a table with the block's name. Let’s add a centreblock:
This text will be centred on the web page. That’s all we have to do. If we wanted to do more, we could have various code and styling blocks, say, accordions, tabs, etc.
Many out-of-the-box blocks are available for Franklin; see Block Collection.Further down the page, you will see Block Party.
One can add teasers, heroes, accordions, tabs, etc. Your developers can create many more.
Metadata
Another feature of Franklin is a special block embedded in the HTML page containing the content and the values to be used as metadata. This block describes the page to external systems and configures the page for the Franklin project.
In each row below, the first column is the metadata keyword, and the second column is the value of the metadata element. We have multiple items: one for the page title, one for the page description; one for an image, and one for a line of extra data. The case is ignored in the first column. The metadata table is typically placed at the foot of the document, though this is not a requirement.
This metadata translates into the final HTML
I will cover metadata in detail in the developer's guide. It's good to know that Franklin covers metadata, that it is easily maintained by the content team, and that it may be used by Google or other indexers. Franklin may also use the metadata to change the page layout.
Preview and publish
Franklin contains a toolbar that allows authorised users to preview or publish pages via the Edge Delivery Services platform.
Pressing Preview allows the content author to see the final page, and then pressing Publish sends the page and its assets—the content, media, and code buses—to the public store.
The publish bus system stores the various elements of the web page in a publicly visible cloud storage system where the code bus provides the final code needed to serve the web page to the public. The Adobe code in the code bus is started by a JavaScript file embedded in the HTML, named aem.js. This orchestrates the delivery and sequencing of the assets, the other code blocks and only the blocks required for the current page. This is a speedy delivery, made faster by a bring-your-own CDN such as Fastly.
The assets bus provides images in a suitably rendered format using HTML picture elements, keeping the website fast and performant on mobile websites.
A properly implemented Edge Delivery Services page should have a great Core Web Vitals or Lighthouse score.Lighthouse performance scoring | Chrome for Developers
The Lighthouse score for this page is below:
The great score 100/100/100/100 comes out of the box. As you add scripts, especially third-party or monitoring scripts, you must continually monitor the performance to ensure the site performs as expected. Adobe provides great tools in the Franklin ecosystem, such as real user monitoring (RUM) and performance checks on GitHub. It helps you ensure you keep world-class speeds. Any Lighthouse score above 80 is at the top of the web.

*** End of Page ***

*** New Page ***
https://allabout.network/blogs/ddt/developer-guide-to-document-authoring-with-edge-delivery-services-part-3

Developer Guide to Document Authoring with Edge Delivery Services. Part 3
Spreadsheets
Indexing
Spreadsheets and Json
Query parameters
Redirects
.helix folder
This is part 3 of my Developer’s Guide to Document Authoring with Edge Delivery Services.
We extended ‘Hello World’ with style variations on the bio block, added fragments, and discussed styling in more depth.
Next - We can use spreadsheets, not just documents.
Within Google Drive or Sharepoint, you can upload or create spreadsheets in Excel or Google Sheets format. If you are using Google Drive, remember to change your settings to convert Excel to Google Sheets automatically,How to Convert Multiple Word Documents/Sheets to Google Docs.
Using spreadsheets
Spreadsheets are versatile tools that can be used for various purposes. In this post, we will explore some of the most common uses for spreadsheets, including:
Source of JSON for your website: Spreadsheets can create JSON data that populates Edge Delivery Services pages or another website using ‘headless technologies’ such as react.js. They are a convenient way to store and manage data that needs to be displayed on a website.
Notification of events from Edge Delivery Services: Spreadsheets can be used to receive notifications of events from Edge Delivery Services. This can be useful for monitoring your website's performance or troubleshooting issues. The Spreadsheets created can be used on your website; we will demo this.
Configuration of Edge Delivery Services: Spreadsheets can be used to configure Edge Delivery Services. This optimises your website's performance or enables specific features like headers, metadata, and authentication.
1 Use a spreadsheet as a source of JSON for websites.
One can create a spreadsheet in MS Excel and upload it to Google Drive or create one directly inside Google Drive. For the demo, we will create a fortune cookie block in our document that displays a random cookie from a JSON file named cookies.
In Google Drive, create a folder named /data; in that folder, create a sheet named cookies.
The first row of the sheet becomes the json attribute names.
We now want a list of cookies, and I will get chatGPT to give me a list of 9
Press the preview button in the sidekick to see a nice json layout; I use json view.
Investigating the structure of the json.
The JSON structure consists of an object with several properties:
1. "total": A number representing the total count of data elements.
2. "offset": A number indicating the starting position of the data subset.
3. "limit": A number specifying the maximum number of data elements in this subset.
4. "data": An array containing the actual data elements. Each element is an object with two properties:
- "key": A string representing the message's short title
- "value": A string containing the actual message.
5. ":type": A string indicating the type of data, in this case, "sheet", the source of the data
In simple terms:
- The object has metadata about the data set, including the total number of elements, the offset and limit for this subset of data (useful for pagination), and the data type.
- The actual data is in the "data" array.
All JSON derived from spreadsheets in Document Authoring with Edge Delivery Services share the same formatting; the main json is in the ‘data’ element, and everything else is metadata.
We would like to build a fortune cookie block.
Start by creating a table ‘FortuneCookie’ in our Hello World document.
Remember that the name cell, the only cell in the case above, is changed to lowercase when used in Edge Delivery Services.
In our GitHub, we need to createblocks/fortunecookie/fortunecookie.css and
blocks/fortunecookie/fortunecookie.js
The code in fortunecookie.js will use the browser's fetch command to read the json, generate a random number, use that number to pluck a key and a value, and then place the results in the block.
Viewing the page
We now have a Fortune Cookie on our page. You can add any JSON to your pages by using spreadsheets when someone presses preview and publish.
Spreadsheets and JSON
In addition to translating Google Docs and Word documents into markdown and HTML markup, AEM also translates spreadsheets (Microsoft Excel workbooks and Google Sheets) into JSON files that your website or web application can easily consume.
This enables many uses for content that is table-oriented or structured.
Sheets and Sheet Structure.
The simplest example of a sheet consists of a table that uses the first row as column names and the subsequent rows as data, as seen above.
Edge Delivery Services allows you to manage workbooks with multiple sheets.
If there is only one sheet, Edge Delivery Services will use that sheet as the default source of information.
Edge Delivery Services will only deliver sheets with sheet names prefixed with helix if there are multiple sheets. This lets you keep additional information and possibly formulas in the same spreadsheet that will not be delivered to the web.
If there is a sheet named helix-default, it is delivered if no additional query parameters are supplied.
See the following for details on how to query a specific sheet.
Query Parameters
Offset and Limit
Spreadsheets and JSON files can become very large. In such cases, Edge Delivery Services supports limit and offset query parameters to indicate which spreadsheet rows are delivered.
As Edge Delivery Services always compresses the JSON, payloads are generally relatively small. Therefore, if the limit query parameter is not specified, the Edge Delivery Services limits the number of rows it returns to 1000 by default. This is sufficient for many simple cases.
Sheet
The sheet query parameter allows an application to specify one or multiple specific sheets in the spreadsheet or workbook. For example,?sheet=jobs will return the sheet named helix-jobs, and ?sheet=jobs&sheet=articles will return the data for helix-jobs and helix-articles.
2 Notification of events from Edge Delivery Services
The empty sheet
Edge Delivery Services keeps a json index of all published pages; in Document Authoring with Edge Delivery Services, you can have a copy of this index by creating a sheet in the root folder of your document store. This sheet must be named query-index and have a single sheet named raw_index. The columns are path, title, image, description and lastmodified. You can choose to add others like ‘author’. See indexing
When you publish a page, Edge Delivery automatically enters it into this spreadsheet with updated attributes and publishes the sheet. It's a two-way traffic sheet.
Using this documentation website as an example
And when we preview
This json file can be used in Edge Delivery Services to create dynamic blocks, where the content is pulled from a json file.
Creating a BlogList, using index-query.json
We added a bloglist block to the document.
We will create the bloglist code at
/blocks/bloglist/bloglist.css and/blocks/bloglist/bloglist.js
Bloglist Javascript
The javascript will also use fetch, which is similar to the fortunecookie block. We want to obtain the json, restrict it to just my blogs, sort by title, and then restrict the final json to three entries. You can see four entries in the sample above.
Create the BlogList style
Previewing the page
3 Configuring Edge Delivery Services
Edge Delivery Services is controlled in many ways; spreadsheets are the content Author-friendly way of performing this task.
You have seen that we created a query-index sheet in our root folder. This is a two-way sheet that receives information from the publishing process and can be used to create dynamic content for your pages.
Edge Delivery Services also has a sheet named redirects, which creates the json entity redirects.json
In this sheet, we have two columns: Source and Destination.
I made the mistake of creating my blogs in the root folder of my Google Drive, and I did not similarly name them, so I moved them to a subtree ‘/blogs/ddt’ and created the redirects sheet in the root of the folder.
I also deleted the original pages, so you are redirected to the new destination when you ask for the older location: nice one, Adobe. Thinking about your hierarchy before you begin is preferable, but knowing that you can change it afterwards is nice.
The .helix folder
You can control the inner workings of Edge Delivery services using the folder named .helix
Note the ‘.’ -- it is dot-helix .helix
Note the name is a previous code name for this technology. The dot in front makes this folder unservable from the internet, which is excellent for secrets like how you are configuring Edge Delivery Services. You could think of the ‘.’ as just like the dot in Unix -- files are hidden!!
Anyway, the files you create in this folder are used for configuring Helix
Headers
The sheet ‘headers’, which in turn ought to create headers,json - you cannot see this file, contains the key and value of the headers you want to send out with your web pages.
My headers contain a content security policy, which helps to detect and mitigate certain types of attacks, such as cross-site scripting (XSS) and data injection attacks.Link to CSP Description on Mozilla
Bringing up the sidekick
When you click on preview, instead of a new set of options, including publish, you get a transient sign ‘Configuration successfully activated’.
Because this is in the .helix folder, it does not need publishing -- it is not visible to the internet.
Config
The next sheet to look at is config; you put interesting things here for your configuration.
Part of mine is shown below; keys and secrets are here to configure CDN and caching; you can also configure authentication. I will not show you my samples for the obvious reasons.
You can find more info about the ‘.helix’ folder and metadata config here.
https://www.aem.live/docs/bulk-metadata
My next post will be about tips and tricks with Json, including building a react application inside Edge Delivery Services.

*** End of Page ***

*** New Page ***
https://allabout.network/blogs/ddt/developer-guide-to-document-authoring-with-edge-delivery-services-part-4

Developer Guide to Document Authoring with Edge Delivery Services. Part 4
Building an SPA app with Json
This is part 4 of my Developer’s Guide to Document Authoring with Edge Delivery Services. This time, we will create an SPA app.
We demonstrated how spreadsheets can serve as a JSON source for websites and Edge Delivery Services, as well as for notifications and configuration purposes.
Part 4 - Building an App with Json
I will first create an application in Edge Delivery Services and then extend this to a React application. Edge Delivery Services supports react and vue; one does not get the perfect 100,100,100,100 lighthouse scoring when we move to Single Page Applications (SPAs), though we get enhanced user interactions. This article shows how Edge Delivery Services can assist customers in building the best of both worlds. Architects should ensure that the home, campaign, and direct landing pages are built with pure Edge Delivery Services code; secondary pages can be more interactive, keeping the website's performance high. Like all rules, you can choose to disregard this one.
The app we build will demonstrate that Edge Delivery Services is not just for static pages.
Specification of the App
The app is a slideshow designed for content authors who create individual pages.
In a ‘traditional CMS, ’ this would probably be a pool of images in an extendable component, which is difficult for a content author to use. We will build a design focused on the content author.
Each page will also serve as a standalone entity, with a visually appealing slide image complemented by detailed textual content. The code's primary objective is to extract metadata from each page, represented as a JSON object. This metadata is then employed to showcase the slides in a continuous scrolling format, providing users with an immersive experience; each slide is linked to its corresponding page, enabling seamless navigation for users seeking additional information or exploring the content in greater depth.This feature enhances the user experience by allowing easy access to supplementary materials, fostering a more interactive and informative environment.
Creating the documents
Create a folder ‘slides’ in the root folder of our Google Drive, and create our page in this folder.
As a subject, I will create slides about York, the English city where I live. I will create five pages with images and information. The code will not be restricted to five pages; use as many as you need.
My pages will be simple to show the context of creating an SPA, and the layout will be
Title, Image, Descriptive text, Metadata
Similar to this, each page is different, and it will expand on the text.
Accessing the pages
Query-index
All pages created? Add a query-index spreadsheet in the folder; we covered this in part three when we created a query-index in the root folder. As a reminder, we want a blank like this.
Now preview and publish the pages.
Edge Delivery Services populates the query-index
We can look at the json
We should create the landing page.
“Five things to do in York, England”
In this page we added a single block ‘slide builder’; which will contain the JavaScript and styling for our SPA.
Remember that Edge Delivery Services replace non-printing, and strange characters with hyphens; we need to create our files in
/blocks/slide-builder/slide-builder.css
/blocks/slide-builder/slide-builder.js
The JS is the first thing I will create. We use ‘fetch’ to obtain the json from our query-index in the slides folder of the Google Drive, iterate over every entry and make the initial page markup.
We will develop the code step by step.
As usual, we start with our export statement; it is good practice to do this
We have started with a query to check if WebP, high-speed images, is supported.
We will layer this up as we go through the tutorial.
Fetch the slides
There are two functions here; the first fetch retrieves the query-index inside the /slides/ folder, which lists the published slide we saw in the json above.
However, each slide page has information we want for the SPA, a hero-type image with variations for mobile etc, the intro para and detailed paragraphs; we need to pick those up too. We bypass this in a mobile view (viewport <800). In the mobile view, the html is replaced by a null. The fetchSlideHtml function appends ‘.plain.html’ to the path; this signals to Edge Delivery Services to treat the page as a fragment and only return plain html without a header footer or any javascript - just plain html.
If returned, it is stored in the json for later use.
Next step, add the slides.
We Iterate over the json and call functions to build our app; we use an intersection observer feature to lazy load our images. That bit that says rootMargin: 100px is the wiggle room for our image coming into view.
Create each slide
The createSlideItem function builds all of the html necessary to display an individual slide, adding a click handler, which creates a panel; if we are running on a desktop, it adds additional supporting text (the first paragraph from the HTML) to the slide.
This technique speeds up mobile whilst keeping the desktop view enriched. The design is mobile-first, desktop-enriched.
The CreatePanel Function
In the createPanel we check to see if the HTML is present to display; if not we fetch it. Remember it is not present on the mobile view; keeping this responsive and fast. We add the controls to dismiss the panel.
FetchSupportingText Function
The FetchSupportingText function reads the HTML, if present, from the html node of the Json, parses it with the Dom Parser and extracts the first paragraph after the first h2 element. This is an essential technique in Edge Delivery Services; the web uses modern javascript, and the browsers fully support parsing DOM structures. Edge Delivery services treat the DOM as a first-class citizen, tying this with ‘plain.html’, and developers have a rich resource set.
Setting the background image
Two things are happening here: the url is constructed from the original image url, which was present in the metadata, and therefore, the image node of the json. We use the supportsWebP variable to adjust the image for WebP serving from Edge Delivery, set width and optimise level through a query string. We go for the original image if the browser does not support WebP.
Notice that we have img.onload and image.onerror functions to support lazy loading.
That was a lot to cover
Here is the complete listing
Completing the ask, the CSS
Phew that is complicated; by breaking it into smaller pieces, I hope you see that the design fits together and you will be able to build SPA Applications with query-index.json and .plain.html
We managed to keep great lighthouse scores despite using lots of fetch and stitching together. Edge Delivery Services is a wonderful tool.
The finished Spa App is here Five things to do in York, England (allabout.network)
In contrast, we can convert this into a ReactJS application and serve it directly from Edge Delivery Services, which will be the next part of this series.
All Developer Articles

*** End of Page ***

*** New Page ***
https://allabout.network/blogs/ddt/developer-guide-to-document-authoring-with-edge-delivery-services-part-5

Developer Guide to Document Authoring with Edge Delivery Services. Part 5
Building a ReactJS app
This is part 5 of my Developer’s Guide to Document Authoring with Edge Delivery Services. This time, we will create a React App that uses Edge Delivery Services as the data source.
We created a SPA app using Documents as the data source and a document with a block to serve the content.
Part 5 - Building a React app with documents as the source
As a developer, you may be more familiar with ReactJs and want to use those skills in Document Authoring with Edge Delivery Services. You may also have an existing ReactJs app that you want to bring into the Edge Delivery Services Infrastructure. Whatever your reason, this tutorial will help you use ReactJs with Edge Delivery.
I walk you through converting the slide builder block we built in part 4 into a modular React app. We'll cover the key steps and considerations to ensure a smooth transition.
1. Understanding the Original Structure
The original approach was a block consisting of a single JavaScript file (slide-builder.js) and a corresponding CSS file (slide-builder.css). This monolithic approach worked well within the Edge Delivery Services ecosystem, it needs modularity to work well with React.
2. Breaking Down the Components
The first step in our conversion is to identify the main components of our application:
- SlideBuilder: The main container for all slides
- SlideItem: Individual slide components
- SlidePanel: The panel that opens when a slide is clicked
3. Setting Up the React Project
Start by creating a new React project. You can use Create React App or your preferred React boilerplate.
After creating a new React project with the Create React App, you can streamline your project by removing several files that aren't necessary for our conversion. Here's a list of files you can safely delete:
1. In the `src` folder:
- `App.test.js` (We are a tutorial, no tests. Do this later)
- `logo.svg` (the React logo, which we won't be using)
- `reportWebVitals.js` (we will not need web vitals reporting)
- `setupTests.js` (no tests, see above)
2. In the `public` folder:
- `logo192.png` and `logo512.png` (React logos)
- `manifest.json` (We are not building a Progressive Web App)
- we can delete favicon.ico
- `robots.txt`
Rename index.html to slides.html
After cleaning up, your project structure might look like this:
This cleaned-up structure provides a good starting point for implementing your converted React application. You can now add your new components and styles to the `src` folder. The final project structure will end up like this.
The index.html<!DOCTYPE html>
Now app.js
Instead of directly fetching data in the decorate function, I used React's useEffect hook in the App component to fetch slide data when the component mounts.
app.css
We employ hooks such as React, useState, and useEffect in React. Additionally, we integrate the SlideBuilder component and the App.css file. The App function component serves as the foundation of our application. With the assistance of the useState hook, we effectively manage the "slides" state. To retrieve slide data upon component mounting, we utilize the useEffect hook. The fetchSlides function, an asynchronous operation, acquires slide data from the "/slides/query-index.json" endpoint. The component structure encompasses a header and a primary section. Within the main section, the SlideBuilder component is rendered, passing slide data as a property.
index.js, remove core web vitals
Change README.md
4. Creating React Components
Now, let's create our React components:
- src/components/SlideBuilder.js
- src/components/SlideItem.js
- src/components/SlidePanel.js
Don’t worry about dangerouslySetInnerHTML. I only obtained a text node in the first place.
Each component encapsulates its logic and render method.
The core logic from the original `decorate` function was distributed among these components:
- Moved slide fetching logic to the main App component
- Implemented background image loading in SlideItem
- Handled panel opening/closing in SlideItem and SlidePanel
5. Styling
Create separate CSS files for each component:
- src/styles/SlideBuilder.css
- src/styles/SlideItem.css
- src/styles/SlidePanel.css
Build the output
To distribute your React application, you must create a production build and then deploy it. Here's a step-by-step guide on what you need to do:
Create a Production Build:
In your project directory, run the following:
npm run build
This command creates a build directory with a production build of your app.
The build directory will contain:
To distribute your app, you need to serve the contents of the build directory.
Returning to our Edge Delivery Services repository, it looks similar to the archetypal boilerplate.
Copy the internal part of the build folder, the static folder and the index.html to the root of the Edge Delivery Repo
Now, the Repo is integrated, as seen below.
We have added slides.html and the static directory.
Now commit and push your Repo.
Conclusion
Converting an Edge Delivery Services block to a React application involves breaking down the monolithic structure into reusable components, adapting the logic to work with React's lifecycle and hooks, and ensuring that all functionality is preserved. While it requires some effort, the result is an extensible codebase that leverages the power of React's component-based architecture. I prefer the block format; this tutorial is to help give you options.
Remember, this conversion is just the beginning. Once in React, you can further enhance your application with additional features, optimize performance, and integrate with other React libraries and tools. If your customer already has a ReactJS application, a calculator, or something more sophisticated, you can easily integrate this into your Edge Delivery Services site collection.
The final ReactJS application, served directly from GitHub, cannot be previewed using the sidekick and thus cannot be published through it. Despite this, it is live. You can view it here https://allabout.network/slides.html

*** End of Page ***

*** New Page ***
https://allabout.network/blogs/ddt/developer-guide-to-document-authoring-with-edge-delivery-services-part-6

Developer Guide to Document Authoring with Edge Delivery Services. Part 6
Json and GitHub
A bit more info on Json, using GitHub as a fallback for sheets
This is part 6 of my Developer’s Guide to Document Authoring with Edge Delivery Services; this time, we will investigate Json and The GitHub connection.
We created a SPA app using Documents as the data source and a document with ReactJS to serve the content.
Part 6 - Json and Github
Edge Delivery Services can create an index of published material; one uses a Google sheet/ Microsoft Excel sheet to do this work. Full details here Indexing (www.aem.live)
If we provide the blank query index in the notes folder, they are entered into the sheet as shown when Edge Delivery Servies creates published pages.
Then the json is produced by the url main--allaboutv2--ddttom.hlx.live/notes/query-index.json
We discussed all of this in an earlier article, in part 3.
The critical thing to note is the structure of the json returned from the sheet; we can create json from a sheet by creating titles and cell entries like this.
This, in turn, will provide simple json
In edge delivery services, any json that is provided in the root folder with the path name of header.json is applied to the website. Edge Delivery Services will use these headers for the paths mentioned in the url element. Note the wildcard. Custom HTTP Response Headers (www.aem.live)
You may not want to give content authors complete control over the headers; it is a complex subject with wildcarding and key/value entries. I suggest making this a developer's responsibility.
If you create a matching headers.json in your code editor and add it to the GitHub root folder, then GitHub will be used if the sheet has yet to be created. The beauty of Edge Delivery services is that if the user is technical and aware of the technique to create their sheet, they can do so, and their version of the json will override the developer-provided one. I use this technique in my development of Edge Delivery Services Projects.
I have configuration information in a file named /config/defaults.json in my GitHub.
I have kept the generated JSON original layout as if I had created it on a sheet. However, I have changed the type to ‘fallback’ so that I can debug it later.
If we need to change things on production without releasing new code, we just have to create a new sheet named defaults with all the information, including the changes and then publish it.
Other sheets used in Edge Delivery Services are
Metadata (Bulk Metadata (www.aem.live))
Redirects (Redirects (www.aem.live))
Config (Project Configuration (www.aem.live)) is a particular item in a special folder. It cannot be served on the web and cannot be previewed.

*** End of Page ***

*** New Page ***
https://allabout.network/blogs/ddt/developer-guide-to-document-authoring-with-edge-delivery-services-part-7

Developer Guide to Document Authoring with Edge Delivery Services. Part 7
Useful Components
Blocks I use in my guides.
Index
Returntotop
Bloglist
I explain the additional blocks I use in my guides.
We discussed Google Sheets/ Excel spreadsheets, Edge Delivery Services automatic conversion to Json, and the ability to create fallbacks in Github.
Part 7 - Useful Components
In building this series, I have created three blocks (or components, in old CMS speak) that help provide navigation widgets.
Index - provides an expanding index of the topics in the article.
Returntotop - provides a back-to-top button when the reader scrolls away from the opening viewport.
Bloglist - provides a list of related articles.
Each block is designed to be intuitive for the user and easy for the content author, automating the work. The content author just has to place the item in the correct place in the document, as a block fragment, queued in by a table. The code achieves this functionality using vanilla JavaScript, CSS 3 and DOM manipulation; no frameworks or libraries are involved - this is the perfect way to create Edge Delivery Services blocks (components).
returntotop
I will begin with the simplest one, returntotop
The function makes the "Back to Top" button visible when the user scrolls down over 100 pixels. The page smoothly scrolls back to the top when the button is clicked.
This block has two cells: the first one is the block name, and the second is the content. The content is the wording the author wants to use when it is displayed; one may wish to use different phrasing or say it in French; it is up to the content author.
As always, create the folder returntotop under the blocks folder, then make two files returntotop.css and returntotop.js
returntotop.css contains the styling.
The returntotop.js contains the code.
It adds an event listener to the window object that listens for the scroll event. The button is displayed when the user scrolls down more than 100 pixels (display: 'block'). Otherwise, the button is hidden (display: 'none').
index
Once again, create a folder inside blocks called index and two files, index.css and index.js, in this folder.
Index.css should contain
And index.js
This code creates an interactive webpage index by selecting all header elements (h1 to h6) and the index block. It creates an index header with a clickable arrow and an index content container. The index is built lazily - only when the user first clicks on the index header. Clicking the index header toggles the visibility of the index content and rotates the arrow.
The buildIndex function creates a list of links to all headers on the page, indenting them based on their header level (h1, h2, etc.). Each header is given a unique ID, and the index links are created to point to these IDs.
Finally
bloglist
Create a folder inside blocks called bloglist and then add the two files bloglist.css and bloglist.js.
Bloglist.css
This CSS creates a responsive blog list layout, using CSS Grid for the blog list, with auto-fitting columns. Each blog item is a flex container with a border. Blog images are set to a fixed height with full width. The last modified date is pushed to the bottom with a separator line. On mobile (< 600px), it switches to a single-column layout.
The Javascript file
The Javascript fetches blog data from "/query-index.json", filtering blog items to include only those with "developer-guide" in the path, sorts the filtered items alphabetically by title, generates HTML content for the blog list and inserts the generated content into the DOM.

*** End of Page ***

*** New Page ***
https://allabout.network/blogs/ddt/developer-guide-to-document-authoring-with-edge-delivery-services-part-8

Developer Guide to Document Authoring with Edge Delivery Services. Part 8
Using AI with Edge Delivery Services
So, if you want to use AI with EDS, AKA Helix or Franklin
You will end up with an AI agent that writes Edge Delivery Services code for you, generating code from text or screenshots.
I explain how to use AI effectively with Edge Delivery Services.
I opened up my useful components.
Part 8 - Using AI with Edge Delivery Services
I constructed this series just as AI became popular, especially for developers creating code. So, I was tempted. I tried writing with AI—this was an abysmal experience, so I had to hand-craft these pieces. You might detect the odd bit of wandering, repeats, and typos. I did have Grammarly on hand to help. I used Cursor.ai to create my examples; they were not generated raw by AI, but AI assisted my programming.
Using Claude without training
Let us see what happens if you jump into Claude and ask for programming code from Claude for Franklin.
Now, this is not intelligent. It has no clue what I meant by Franklin. It knows block and accordion and then hallucinates that we want ‘a react component’. No, we don't. We need plain modern JavaScript and CSS3.
Perhaps the word Franklin is not working; try again
Same answer.
Training Claude
Ok, so we need to train Claude. There are three good sources of training material,
Adobe Experience Manager (www.aem.live)
Developer Guide to Document Authoring with Edge Delivery Services - Part 0 (allabout.network)
Your Repo, or perhaps you are starting from scratch GitHub - adobe/aem-boilerplate: Use this repository template for new AEM projects.
When I started writing Part 8 (this part), I realised I had not given enough context for an AI to understand what I was doing; therefore, I wrote Part 0 to fill in the gap. I also needed to provide more background for humans.
The aem.live site is good and covers the topic well, but it is not something one can feed to an AI.
So, I wrote a site scraper that extracts the text elements and links in a site by scraping and caching the web page; it extracts the text elements from a page (ignoring navigation), extracts all links (including navigation), and marks the ones that have been followed. As it goes along, It writes to a text file.
The source code for my scraper, which extracts relevant text pieces from an entire site
NodeJs src for a scraper that constructs a document to train AI.
And the config.js that informs what to exclude
I ran this code against aem.live and Part 0 of this tutorial; it diligently created files that contained the site's essence.
Adding the correct parts of your repo to the AI training
The final part of the training is the work already done, your repo.
The AI should be interested in the blocks you have added to your repo and the base style sheets.
I wrote another nodejs app that scans your folders and builds instructable text from CSS, JS, and MD. I believe in using MD files next to blocks to keep documentation within the implementation.
NodeJs src for a scanner that collects JS, CSS, and MD.
I ran this
node index.js -i ~/Documents/GitHub/{myrepname} -o ./output -f result.txt -l debug
Creating the final training file
I have five more manually created files, which provide context for the machine-generated files.
developer-front.txt
This part of the instructions is intended to train an ai in the coding style for an adobe product. The product uses CSS3, modern javascript and DOM manipulation instead of libraries
Instructions
Edge Delivery Services (EDS) Overview:
EDS is Adobe's serverless platform for fast, scalable web content delivery. Also known as Franklin or Helix. It allows content creators to use familiar tools like Google Docs/Microsoft Word for authoring. The system converts documents to HTML/JSON and serves them via CDN. EDS supports blocks/components for layout and functionality, enabling rapid development and publishing.
Key Components:
1. Document-based authoring
2. Auto-blocking and metadata handling
3. JSON generation from spreadsheets
4. Redirects and configuration options
Development Process:
1. Content Creation:
- Use Google Docs or Microsoft Word
- Add metadata tables for page properties
- Define blocks using tables in the document
2. Preview and Publish:
- Use Sidekick tool in document
- Preview generates temporary URL
- Publish pushes content to production
3. Block Development:
- Create folder in /blocks with block-name.css and block-name.js files, inside a folder called block-name. e.g. /blocks/example/example.js and /blocks/example/example.css and /blocks/example/README.md
- Use vanilla JavaScript and DOM manipulation
- Access block content via classes and data attributes, never ID
4. Spreadsheet Usage:
- Create spreadsheets for dynamic content or configuration
- System automatically converts to JSON
- Access via /path/to/spreadsheet.json
5. Advanced Development:
- Use query-index.json for dynamic content across site
- Configure headers, redirects, authentication in .helix folder
Block Development Details:
1. CSS:
- Use CSS3 features including variables
- Create variations with additional classes
2. JavaScript:
- Export a default function that decorates the block
- Use modern browser APIs (fetch, Intersection Observer, etc.)
- Avoid external libraries to maintain performance
3. HTML Structure:
- Blocks wrapped in <div class="blockname-wrapper">
- Inner content in <div class="blockname block">
Best Practices:
1. Performance:
- Leverage EDS features for optimal page speed
- Minimize external dependencies
- Use lazy loading for images and heavy content
2. Modularity:
- Keep blocks self-contained and reusable
- Use block variations for different use cases
- Separate concerns (HTML structure, styling, behavior)
3. Content Author Experience:
- Make block usage intuitive in documents
- Provide clear documentation for custom blocks
- Use sensible defaults with options for customization
4. SEO and Accessibility:
- Ensure proper heading structure
- Use semantic HTML elements
- Provide alt text for images
5. Responsive Design:
- Design mobile-first, then enhance for larger screens
- Use CSS Grid and Flexbox for layouts
- Implement responsive images with picture element
Configuration and Advanced Features:
1. .helix folder:
- Store configuration files (headers, redirects, etc.)
- Not publicly accessible
2. Spreadsheets and JSON:
- query-index for site-wide content index
- Use for dynamic content, site configuration
- GitHub fallback for spreadsheets
3. Authentication:
- Configure in .helix/config spreadsheet
4. Redirects:
- Manage in redirects spreadsheet or GitHub fallback
5. Custom HTTP Headers:
- Define in headers spreadsheet or GitHub fallback
system-block-ai-front.txt
# Guide to Creating Blocks in Edge Delivery Services (EDS) or Franklin, Helix
Blocks are modular components used in development to create reusable and customizable sections of a webpage.
## Block Structure
- Each block typically consists of two main files, and an optional third:
1. A CSS file (e.g., `blockname.css`) for styling
2. A JavaScript file (e.g., `blockname.js`) for functionality
3. a README.md for documentation
If asked to create a block, one should always create a README.md alongside it
## Key Components of a Block
### CSS File
- Contains styles specific to the block
- Often includes responsive design for different screen sizes
- May use CSS variables for theming
### JavaScript File
- Exports a `decorate` function as the default
- The `decorate` function typically:
- Manipulates the DOM to create the block's structure
- Adds event listeners for interactivity
- Fetches data if needed (e.g., from JSON files)
- Applies dynamic styling
### Markdown File
- Contains documentation of use to the developer, maintainer or content author
### Common Patterns
- Use of `document.createElement` to create elements
- Applying classes for styling
- Creating nested structures (e.g., for carousels or grids)
- Fetching and processing data (often using `fetch` API)
- Implementing responsive behavior
## Best Practices
1. Keep blocks modular and reusable
2. Use semantic HTML where possible
3. Implement accessibility features (ARIA attributes, keyboard navigation)
4. Optimize for performance (lazy loading, efficient DOM manipulation)
5. Follow a consistent naming convention for classes and IDs
## Advanced Features
- Some blocks may use external libraries or APIs
- Complex blocks may implement lazy loading or infinite scrolling
- Interactive blocks often use event delegation for efficiency
## Integration
- Blocks are typically integrated into a larger project structure
- They may interact with a content management system or data layer
- Often used in conjunction with a main JavaScript file that handles overall page behavior
This guide provides a high-level overview of creating blocks in web development. Each block type has its own specific implementation details and requirements.
What follows is sample blocks from my GitHub repository
system-block-ai-foot.txt
##### This is the end of the examples
When asked to create anything, if the result is a block, remember to create a README.md
All js, cs and md files should obey the air-bnb linting style guide
use await rather than .then() for asynch code
always list every line and function in code
lib-franklin.js has been renamed aem.js; remember to check this in the generated code and replace it if necessary
Finally, my Readme.txt places them all in the correct order; I have named the result file all.out to remind me it is not just a plain text file. It contains magic.
Cat.sh and ReadMe.txt
cat aem-live-result.txt system-developer-front.txt developer-guide.txt system-block-ai-front.txt block-ai-result.txt system-block-ai-foot.txt >all.out
This Unix/Linux shell command combines the contents of multiple text files into a single output file. Let's break it down:
1. `cat`: This command concatenates (combines) and displays the contents of files.
2. The files being combined are:
- `aem-live-result.txt`
- `system-developer-front.txt`
- `developer-guide.txt`
- `system-block-ai-front.txt`
- `block-ai-result.txt`
- `system-block-ai-foot.txt`
3. `>`: This is an output redirection operator. It takes the output that would normally be displayed on the screen and instead writes it to a file.
4. `all.out` is the name of the output file to which the combined contents of all the input files will be written.
So, when you run this command, it will:
1. Read the contents of the files in the correct order.
2. Concatenating all of their contents.
3. Write the combined content to a new file named `all.out`.
If `all.out` exists, its contents will be overwritten. If it doesn't exist, it will be created.
This takes all of the files Tom Cranstoun created and turns them into an `all.out` file for Claude or other AI.
When added as context, this turns Claude into an agent that can create blocks for Adobe Edge Delivery Services (AKA Franklin or Helix)
If you want your repo (`block-ai.txt`) or training, you can replace any files except 'system-XXXX--xxx.txt' and rerun.
The file all.out is uploaded to Claude, and the question is given again.
Claude does the trick, including a README.md for the component.
You do not need to create your training files; you can use mine.
Here are the links to the Training texts.
Training Texts
You can cherry-pick, download the whole lot, or use the file all.out, which contains the magic.
With this training, you can also upload images and screenshots into Claude; Claude will then generate complex blocks for you.
I took this screenshot from my home screen on Edge (The browser, gosh, don’t you hate the way this industry names things)
I pasted into my trained Claude,
The first pass was not complete; it said things like​​function createFeaturedStories(stories) {
I said, show me the code, and voila.
Note that I have trained Claude to use airbnb style guide, which is Adobe’s Linting in the GitHub pipeline; this code will pass the linting test.
New task
You now have an agent that can create Edge Delivery Services Blocks with a prompt and maybe a picture; this is a very handy tool. Remember that it is AI-generated, so Mistakes can happen; check the code.
A reminder

*** End of Page ***
sample blocks from my GitHub repository

This is the Markdown file that generates a fraction of the block named README. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/bio/README.md
# bio block javascript

The `decorate` function takes a `block` parameter and performs the following steps:

First of all it checks for the class 'hide-author', if not found it performs the task below]

1. It searches for an `<img>` element within an element that has the class `.bio.block`.

2. If the `<img>` element is found and it has a non-empty `alt` attribute, the function extracts the author name from the `alt` attribute.

3. If the author name is not found in the `alt` attribute or if the `<img>` element doesn't exist, the function looks for a `<meta>` tag with the `name` attribute set to `"author"`. If found, it retrieves the author name from the `content` attribute of the `<meta>` tag.

4. The function then creates a new `<strong>` element and sets its text content to the author name.

5. Finally, the function locates the element with the class `.bio.block` and appends the newly created `<strong>` element containing the author name as the last child of the `.bio.block` element.

In essence, this part is responsible for extracting the author name from either the `alt` attribute of an image or the `content` attribute of a `<meta>` tag, and then adding the author name as a `<strong>` element to the end of the element with the class `.bio.block`; if not hidden.

The wrapper is added to the expressions resolver; it obeys the expression {{expand,$NAMESPACE:VARIABLE$})

It is assumed that the $system:enableprofilevariables$ has been set to 'y' and there are meaning profile variables

This is the CSS file that generates a fraction of the block named bio. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/bio/bio.css
.bio-wrapper .bio.block img {
border-radius: 50%;
object-fit: cover;
}

.bio-wrapper .bio.block.highlighted img {
border: 2px solid blue;
box-sizing: border-box;
}

.bio>div {
display: flex;
align-items: center;
gap: 20px;
}

.bio>div>div:first-child {
flex-shrink: 0;
}

.bio>div>div:last-child {
flex-grow: 1;
}
This is the JS file that generates a fraction of the block named bio. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/bio/bio.js
/* eslint-disable import/no-unresolved */
/* eslint-disable import/no-absolute-path */
import { renderExpressions } from '/plusplus/plugins/expressions/src/expressions.js';

// eslint-disable-next-line no-unused-vars
export default function decorate(block) {
const bioElement = document.querySelector('.bio');

if (!bioElement.classList.contains('hide-author')) {
// Find the <img> element within the .bio.block
const imgElement = document.querySelector('.bio.block img');

let author = '';

// Check if the <img> element has a non-empty alt attribute
if (imgElement && imgElement.getAttribute('alt')) {
author = imgElement.getAttribute('alt');
}

// If the alt attribute is empty or not present, fall back to the <meta> tag's author content
if (!author) {
const metaAuthor = document.querySelector('meta[name="author"]');
if (metaAuthor) {
author = metaAuthor.getAttribute('content');
}
}

// Create a new <strong> element to hold the author name
const authorElement = document.createElement('strong');
authorElement.textContent = author;

// Find the .bio.block element
const bioBlock = document.querySelector('.bio.block');

// Insert the author element as the last child of the .bio.block element
bioBlock.appendChild(authorElement);
}
renderExpressions(document.querySelector('.bio-wrapper'));
}

This is the CSS file that generates a fraction of the block named bloglist. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/bloglist/bloglist.css
.bloglist {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}

.blog-item {
border: 1px solid #ccc;
padding: 20px;
box-sizing: border-box;
display: flex;
flex-direction: column;
}

.blog-item img {
width: 100%;
height: 280px;
object-fit: cover;
margin-bottom: 10px;
}

.blog-item p {
margin: 10px 0;
}

.blog-item .last-modified {
margin-top: auto;
padding-top: 10px;
position: relative;
}

.blog-item .last-modified::before {
content: '';
position: absolute;
top: 0;
left: -20px;
right: -20px;
border-top: 1px solid #ccc;
}

@media screen and (max-width: 600px) {
.bloglist {
grid-template-columns: 1fr;
}
}
This is the JS file that generates a fraction of the block named bloglist. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/bloglist/bloglist.js
export default async function decorate(block) {
const blogListElement = document.querySelector(".bloglist");
const url = "/query-index.json";
const currentPath = window.location.pathname; // Get the current document's path

try {
const response = await fetch(url);
const data = await response.json();

// Filter the blog items based on the presence of "/blogs/ddt/a-developer" in the path
// and exclude the current document
const filteredBlogItems = data.data.filter((item) =>
item.path.includes("developer-guide") && item.path !== currentPath
);

// Sort the filtered blog items by title
const sortedBlogItems = filteredBlogItems.sort((a, b) =>
a.title.localeCompare(b.title)
);


const limitedBlogItems = sortedBlogItems.slice(0, 4);

// generate the content
const content = generateContent(sortedBlogItems);

blogListElement.innerHTML = content;
} catch (error) {
console.error("Error fetching the JSON data:", error);
}
}
function generateContent(blogItems) {
let content = "";

blogItems.forEach((item) => {
const lastModifiedDate = new Date(item.lastModified * 1000);
const formattedDate = formatDate(lastModifiedDate);

content += `
<div class="blog-item">
<a href="${item.path}">
<img src="${item.image}" alt="${item.title}">
<strong>${item.title}</strong>
</a>
<p>${item.description}</p>
<p class="last-modified">Last Modified: ${formattedDate}</p>
</div>
`;
});

return content;
}

function formatDate(date) {
const day = String(date.getDate()).padStart(2, "0");
const month = getMonthName(date.getMonth());
const year = date.getFullYear();

return `${day}/${month}/${year}`;
}

function getMonthName(monthIndex) {
const monthNames = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];

return monthNames[monthIndex];
}

This is the CSS file that generates a fraction of the block named cards. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/cards/cards.css
.cards > ul {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(278px, 1fr));
grid-gap: 16px;
}

.cards > ul > li {
border: 1px solid var(--dark-color);
background-color: var(--background-color)
}

.cards .cards-card-body {
margin: 16px;
}

.cards .cards-card-image {
line-height: 0;
}

.cards .cards-card-body > *:first-child {
margin-top: 0;
}

.cards > ul > li img {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
}

This is the JS file that generates a fraction of the block named cards. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/cards/cards.js
import { createOptimizedPicture } from '../../scripts/aem.js';

export default function decorate(block) {
/* change to ul, li */
const ul = document.createElement('ul');
[...block.children].forEach((row) => {
const li = document.createElement('li');
while (row.firstElementChild) li.append(row.firstElementChild);
[...li.children].forEach((div) => {
if (div.children.length === 1 && div.querySelector('picture')) div.className = 'cards-card-image';
else div.className = 'cards-card-body';
});
ul.append(li);
});
ul.querySelectorAll('img').forEach((img) => img.closest('picture').replaceWith(createOptimizedPicture(img.src, img.alt, false, [{ width: '750' }])));
block.textContent = '';
block.append(ul);
}

This is the Markdown file that generates a fraction of the block named README. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/carousel/README.md
# This block

This block is party of Adobe blocks, not included in the boilerplate; often used in projects, so added here for convenience

This is the CSS file that generates a fraction of the block named carousel. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/carousel/carousel.css
.carousel .carousel-slides-container {
position: relative;
}

.carousel .carousel-slides,
.carousel .carousel-slide-indicators {
list-style: none;
margin: 0;
padding: 0;
}

.carousel .carousel-slides {
display: flex;
scroll-behavior: smooth;
scroll-snap-type: x mandatory;
overflow: scroll clip;
}

.carousel .carousel-slides::-webkit-scrollbar {
display: none;
}

.carousel .carousel-slide {
flex: 0 0 100%;
scroll-snap-align: start;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
position: relative;
width: 100%;
min-height: min(40rem, calc(100svh - var(--nav-height)));
}

.carousel .carousel-slide:has(.carousel-slide-content[data-align="center"]) {
align-items: center;
}

.carousel .carousel-slide:has(.carousel-slide-content[data-align="right"]) {
align-items: flex-end;
}

.carousel .carousel-slide .carousel-slide-image picture {
position: absolute;
inset: 0;
}

.carousel .carousel-slide .carousel-slide-image picture > img {
height: 100%;
width: 100%;
object-fit: cover;
}

.carousel .carousel-slide .carousel-slide-content {
z-index: 1;
padding: 1rem;
margin: 1.5rem 3rem;
color: white;
background-color: rgba(0 0 0 / 50%);
position: relative;
width: var(--slide-content-width, auto);
}

.carousel .carousel-slide-indicators {
display: flex;
justify-content: center;
gap: 0.5rem;
}

.carousel .carousel-slide-indicator button {
width: 1rem;
height: 1rem;
padding: 0;
border-radius: 1rem;
background-color: rgba(0 0 0 / 25%);
}

.carousel .carousel-slide-indicator button:disabled,
.carousel .carousel-slide-indicator button:hover,
.carousel .carousel-slide-indicator button:focus-visible {
background-color: rgba(0 0 0 / 80%);
}

.carousel .carousel-slide-indicator span,
.carousel .carousel-navigation-buttons span {
border: 0;
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
white-space: nowrap;
}

.carousel .carousel-navigation-buttons {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 0.5rem;
right: 0.5rem;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 1;
}

/* stylelint-disable-next-line no-descending-specificity */
.carousel .carousel-navigation-buttons button {
border-radius: 8px;
margin: 0;
padding: 0;
width: 2rem;
height: 2rem;
position: relative;
background-color: rgba(0 0 0 / 25%);
}

.carousel .carousel-navigation-buttons button:hover,
.carousel .carousel-navigation-buttons button:focus-visible {
background-color: rgba(0 0 0 / 80%);
}

.carousel .carousel-navigation-buttons button::after {
display: block;
content: "";
border: 3px white solid;
border-bottom: 0;
border-left: 0;
height: 0.75rem;
width: 0.75rem;
position: absolute;
top: 50%;
left: calc(50% + 3px);
transform: translate(-50%, -50%) rotate(-135deg);
}

.carousel .carousel-navigation-buttons button.slide-next::after {
transform: translate(-50%, -50%) rotate(45deg);
left: calc(50% - 3px);
}

@media (width >= 600px) {
.carousel .carousel-navigation-buttons {
left: 1rem;
right: 1rem;
}

.carousel .carousel-navigation-buttons button {
width: 3rem;
height: 3rem;
}

.carousel .carousel-navigation-buttons button::after {
width: 1rem;
height: 1rem;
}

.carousel .carousel-slide .carousel-slide-content {
--slide-content-width: 50%;

margin: 2.5rem 5rem;
}

.carousel .carousel-slide .carousel-slide-content[data-align="justify"] {
--slide-content-width: auto;
}
}

This is the JS file that generates a fraction of the block named carousel. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/carousel/carousel.js
import { fetchPlaceholders } from '../../scripts/aem.js';

function updateActiveSlide(slide) {
const block = slide.closest('.carousel');
const slideIndex = parseInt(slide.dataset.slideIndex, 10);
block.dataset.activeSlide = slideIndex;

const slides = block.querySelectorAll('.carousel-slide');

slides.forEach((aSlide, idx) => {
aSlide.setAttribute('aria-hidden', idx !== slideIndex);
aSlide.querySelectorAll('a').forEach((link) => {
if (idx !== slideIndex) {
link.setAttribute('tabindex', '-1');
} else {
link.removeAttribute('tabindex');
}
});
});

const indicators = block.querySelectorAll('.carousel-slide-indicator');
indicators.forEach((indicator, idx) => {
if (idx !== slideIndex) {
indicator.querySelector('button').removeAttribute('disabled');
} else {
indicator.querySelector('button').setAttribute('disabled', 'true');
}
});
}

function showSlide(block, slideIndex = 0) {
const slides = block.querySelectorAll('.carousel-slide');
let realSlideIndex = slideIndex < 0 ? slides.length - 1 : slideIndex;
if (slideIndex >= slides.length) realSlideIndex = 0;
const activeSlide = slides[realSlideIndex];

activeSlide.querySelectorAll('a').forEach((link) => link.removeAttribute('tabindex'));
block.querySelector('.carousel-slides').scrollTo({
top: 0,
left: activeSlide.offsetLeft,
behavior: 'smooth',
});
}

function bindEvents(block) {
const slideIndicators = block.querySelector('.carousel-slide-indicators');
if (!slideIndicators) return;

slideIndicators.querySelectorAll('button').forEach((button) => {
button.addEventListener('click', (e) => {
const slideIndicator = e.currentTarget.parentElement;
showSlide(block, parseInt(slideIndicator.dataset.targetSlide, 10));
});
});

block.querySelector('.slide-prev').addEventListener('click', () => {
showSlide(block, parseInt(block.dataset.activeSlide, 10) - 1);
});
block.querySelector('.slide-next').addEventListener('click', () => {
showSlide(block, parseInt(block.dataset.activeSlide, 10) + 1);
});

const slideObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) updateActiveSlide(entry.target);
});
}, { threshold: 0.5 });
block.querySelectorAll('.carousel-slide').forEach((slide) => {
slideObserver.observe(slide);
});
}

function createSlide(row, slideIndex, carouselId) {
const slide = document.createElement('li');
slide.dataset.slideIndex = slideIndex;
slide.setAttribute('id', `carousel-${carouselId}-slide-${slideIndex}`);
slide.classList.add('carousel-slide');

row.querySelectorAll(':scope > div').forEach((column, colIdx) => {
column.classList.add(`carousel-slide-${colIdx === 0 ? 'image' : 'content'}`);
slide.append(column);
});

const labeledBy = slide.querySelector('h1, h2, h3, h4, h5, h6');
if (labeledBy) {
slide.setAttribute('aria-labelledby', labeledBy.getAttribute('id'));
}

return slide;
}

let carouselId = 0;
export default async function decorate(block) {
carouselId += 1;
block.setAttribute('id', `carousel-${carouselId}`);
const rows = block.querySelectorAll(':scope > div');
const isSingleSlide = rows.length < 2;

const placeholders = await fetchPlaceholders();

block.setAttribute('role', 'region');
block.setAttribute('aria-roledescription', placeholders.carousel || 'Carousel');

const container = document.createElement('div');
container.classList.add('carousel-slides-container');

const slidesWrapper = document.createElement('ul');
slidesWrapper.classList.add('carousel-slides');
block.prepend(slidesWrapper);

let slideIndicators;
if (!isSingleSlide) {
const slideIndicatorsNav = document.createElement('nav');
slideIndicatorsNav.setAttribute('aria-label', placeholders.carouselSlideControls || 'Carousel Slide Controls');
slideIndicators = document.createElement('ol');
slideIndicators.classList.add('carousel-slide-indicators');
slideIndicatorsNav.append(slideIndicators);
block.append(slideIndicatorsNav);

const slideNavButtons = document.createElement('div');
slideNavButtons.classList.add('carousel-navigation-buttons');
slideNavButtons.innerHTML = `
<button type="button" class= "slide-prev" aria-label="${placeholders.previousSlide || 'Previous Slide'}"></button>
<button type="button" class="slide-next" aria-label="${placeholders.nextSlide || 'Next Slide'}"></button>
`;

container.append(slideNavButtons);
}

rows.forEach((row, idx) => {
const slide = createSlide(row, idx, carouselId);
slidesWrapper.append(slide);

if (slideIndicators) {
const indicator = document.createElement('li');
indicator.classList.add('carousel-slide-indicator');
indicator.dataset.targetSlide = idx;
indicator.innerHTML = `<button type="button"><span>${placeholders.showSlide || 'Show Slide'} ${idx + 1} ${placeholders.of || 'of'} ${rows.length}</span></button>`;
slideIndicators.append(indicator);
}
row.remove();
});

container.append(slidesWrapper);
block.prepend(container);

if (!isSingleSlide) {
bindEvents(block);
}
}

This is the CSS file that generates a fraction of the block named columns. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/columns/columns.css
.columns > div {
display: flex;
flex-direction: column;
}

.columns img {
width: 100%;
}

.columns > div > div {
order: 1;
}

.columns > div > .columns-img-col {
order: 0;
}

.columns > div > .columns-img-col img {
display: block;
}

@media (width >= 900px) {
.columns > div {
align-items: center;
flex-direction: unset;
gap: 32px;
}

.columns > div > div {
flex: 1;
order: unset;
}
}

This is the JS file that generates a fraction of the block named columns. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/columns/columns.js
export default function decorate(block) {
const cols = [...block.firstElementChild.children];
block.classList.add(`columns-${cols.length}-cols`);

// setup image columns
[...block.children].forEach((row) => {
[...row.children].forEach((col) => {
const pic = col.querySelector('picture');
if (pic) {
const picWrapper = pic.closest('div');
if (picWrapper && picWrapper.children.length === 1) {
// picture is only content in column
picWrapper.classList.add('columns-img-col');
}
}
});
});
}

This is the CSS file that generates a fraction of the block named dashboard. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/dashboard/dashboard.css
.dashboard {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
font-size: 14px;
}

.dashboard h1 {
font-size: 24px;
margin-bottom: 20px;
}

.filter-container {
margin-bottom: 20px;
}

.filter-container label {
font-weight: bold;
margin-right: 10px;
}

.filter-container select {
padding: 5px;
font-size: 14px;
border: 1px solid #ddd;
border-radius: 4px;
}

.content-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}

.content-table th,
.content-table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}

.content-table th {
background-color: #f2f2f2;
font-weight: bold;
cursor: pointer;
position: relative;
padding-right: 20px; /* Make room for sort indicator */
}

.content-table th::after {
content: '\2195'; /* Unicode for up/down arrow */
position: absolute;
right: 5px;
top: 50%;
transform: translateY(-50%);
font-size: 0.8em;
color: #999;
}

.content-table th.asc::after {
content: '\2191'; /* Unicode for up arrow */
color: #333;
}

.content-table th.desc::after {
content: '\2193'; /* Unicode for down arrow */
color: #333;
}

.content-table tr:nth-child(even) {
background-color: #f9f9f9;
}

.content-table tr:hover {
background-color: #f5f5f5;
}

.path-cell {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.path-link {
color: #0066cc;
text-decoration: none;
}

.path-link:hover {
text-decoration: underline;
}

.date-cell {
white-space: nowrap;
}

.image-popup {
display: none;
position: fixed;
z-index: 1000;
background-color: white;
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
padding: 5px;
}

.image-popup img {
max-width: 300px;
max-height: 300px;
display: block;
}

@media (max-width: 1023px) {
.card-layout {
border: none;
}

.card-layout thead {
display: none;
}

.card-layout,
.card-layout tbody,
.card-layout tr,
.card-layout td {
display: block;
width: 100%;
box-sizing: border-box;
}

.card-layout tr {
margin-bottom: 15px;
border: 1px solid #ddd;
border-radius: 4px;
overflow: hidden;
background-color: #f9f9f9;
}

.card-layout td {
position: relative;
padding: 10px 10px 10px 120px;
min-height: 30px;
text-align: left;
border: none;
border-bottom: 1px solid #eee;
}

.card-layout td:last-child {
border-bottom: none;
}

.card-layout td::before {
content: attr(data-label);
position: absolute;
left: 6px;
width: 110px;
padding-right: 10px;
white-space: nowrap;
text-align: left;
font-weight: bold;
}

.card-layout .path-cell,
.card-layout .description-cell,
.card-layout .title-cell {
max-width: none;
overflow: visible;
text-overflow: clip;
white-space: normal;
word-wrap: break-word;
}

.card-layout .path-link {
word-break: break-all;
}

.card-layout .date-cell {
display: flex;
justify-content: flex-end;
align-items: center;
white-space: nowrap;
}
}

.green { background-color: #90EE90; }
.amber { background-color: #FFBF00; }
.red { background-color: #FF6347; }
This is the JS file that generates a fraction of the block named dashboard. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/dashboard/dashboard.js
export default function decorate(block) {
const dashboardContainer = block.querySelector('.dashboard-container') || block;
const jsonUrl = '/query-index.json';
let mouseX = 0;
let mouseY = 0;
let activePopup = null;
let data = null;

// Fetch JSON data and create dashboard
fetch(jsonUrl)
.then(response => response.json())
.then(jsonData => {
data = jsonData.data;
const dashboardElement = createDashboard(data);
dashboardContainer.appendChild(dashboardElement);
addEventListeners();
handleResponsiveLayout();

// Sort initially by Title
sortTable(0, true);
document.querySelector('.content-table th[data-column="0"]').classList.add('asc');
})
.catch(error => console.error('Error fetching data:', error));

function createDashboard(data) {
const dashboard = document.createElement('div');
dashboard.className = 'dashboard';

const title = document.createElement('h1');
title.textContent = 'Edge Delivery Services Content Dashboard';
dashboard.appendChild(title);

const filter = createFilter();
dashboard.appendChild(filter);

const table = createTable(data);
dashboard.appendChild(table);

return dashboard;
}

function createFilter() {
const filterContainer = document.createElement('div');
filterContainer.className = 'filter-container';

const label = document.createElement('label');
label.textContent = 'Filter by status: ';
filterContainer.appendChild(label);

const select = document.createElement('select');
select.id = 'status-filter';

['All', 'Green', 'Amber', 'Red'].forEach(option => {
const optionElement = document.createElement('option');
optionElement.value = option.toLowerCase();
optionElement.textContent = option;
select.appendChild(optionElement);
});

filterContainer.appendChild(select);
return filterContainer;
}

function createTable(data) {
const table = document.createElement('table');
table.className = 'content-table';

const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
['Title', 'Path', 'Description', 'Last Modified', 'Review', 'Expiry'].forEach((text, index) => {
const th = document.createElement('th');
th.textContent = text;
th.className = 'sortable';
th.dataset.column = index;
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);

const tbody = document.createElement('tbody');
data.forEach(item => {
const row = createTableRow(item);
tbody.appendChild(row);
});
table.appendChild(tbody);

return table;
}

function createTableRow(item) {
const row = document.createElement('tr');

const titleCell = createCell(item.title, 'title-cell');
const pathCell = createPathCell(item.path, item.image);
const descriptionCell = createCell(item.description, 'description-cell');
const lastModifiedCell = createDateCell(item.lastModified, 'last-modified-cell');
const reviewDateCell = createDateCell(calculateReviewDate(item.lastModified), 'review-date-cell');
const expiryDateCell = createDateCell(calculateExpiryDate(item.lastModified), 'expiry-date-cell');

titleCell.setAttribute('data-label', 'Title');
pathCell.setAttribute('data-label', 'Path');
descriptionCell.setAttribute('data-label', 'Description');
lastModifiedCell.setAttribute('data-label', 'Last Modified');
reviewDateCell.setAttribute('data-label', 'Review');
expiryDateCell.setAttribute('data-label', 'Expiry');

row.appendChild(titleCell);
row.appendChild(pathCell);
row.appendChild(descriptionCell);
row.appendChild(lastModifiedCell);
row.appendChild(reviewDateCell);
row.appendChild(expiryDateCell);

return row;
}

function createCell(text, className) {
const cell = document.createElement('td');
cell.textContent = text;
cell.className = className;
return cell;
}

function createPathCell(path, image) {
const cell = document.createElement('td');
cell.className = 'path-cell';

const link = document.createElement('a');
link.href = path;
link.className = 'path-link';
link.textContent = path.length > 20 ? path.substring(0, 17) + '...' : path;
link.title = path;

if (image) {
const popup = document.createElement('div');
popup.className = 'image-popup';
const img = document.createElement('img');
img.src = image;
img.alt = 'Preview';
popup.appendChild(img);
link.appendChild(popup);
}

cell.appendChild(link);
return cell;
}

function createDateCell(date, className) {
const cell = document.createElement('td');
cell.className = `date-cell ${className}`;
const parsedDate = parseDate(date);
const formattedDate = parsedDate ? formatDate(parsedDate) : 'Invalid Date';
cell.textContent = formattedDate;

if (formattedDate === 'Invalid Date') {
cell.classList.add('red');
} else if (className === 'review-date-cell' || className === 'expiry-date-cell') {
const daysUntil = Math.ceil((parsedDate - new Date()) / (1000 * 60 * 60 * 24));
if (daysUntil < 0) {
cell.classList.add('red');
} else if (daysUntil <= 30) {
cell.classList.add('amber');
} else {
cell.classList.add('green');
}
}

return cell;
}

function parseDate(dateInput) {
if (dateInput instanceof Date) {
return dateInput;
}
if (typeof dateInput === 'number' || (typeof dateInput === 'string' && !isNaN(dateInput))) {
const timestamp = typeof dateInput === 'string' ? parseInt(dateInput, 10) : dateInput;
return new Date(timestamp * 1000);
}
const parsedDate = new Date(dateInput);
return isNaN(parsedDate.getTime()) ? null : parsedDate;
}

function formatDate(date) {
if (!(date instanceof Date) || isNaN(date.getTime())) {
return 'Invalid Date';
}
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}

function calculateReviewDate(lastModified) {
if (!lastModified) {
return 'Invalid Date';
}
const reviewPeriod = parseInt(window.siteConfig?.['$co:defaultreviewperiod']) || 300;
const lastModifiedDate = parseDate(lastModified);
if (!lastModifiedDate) {
return 'Invalid Date';
}
return new Date(lastModifiedDate.getTime() + reviewPeriod * 24 * 60 * 60 * 1000);
}

function calculateExpiryDate(lastModified) {
if (!lastModified) {
return 'Invalid Date';
}
const expiryPeriod = parseInt(window.siteConfig?.['$co:defaultexpiryperiod']) || 365;
const lastModifiedDate = parseDate(lastModified);
if (!lastModifiedDate) {
return 'Invalid Date';
}
return new Date(lastModifiedDate.getTime() + expiryPeriod * 24 * 60 * 60 * 1000);
}

function addEventListeners() {
window.addEventListener('resize', handleResponsiveLayout);
document.getElementById('status-filter').addEventListener('change', filterTable);
document.addEventListener('mousemove', updateMousePosition);
const pathLinks = document.querySelectorAll('.path-link');
pathLinks.forEach(link => {
link.addEventListener('mouseenter', showPopup);
link.addEventListener('mouseleave', hidePopup);
});
const headers = document.querySelectorAll('.content-table th');
headers.forEach(header => {
header.addEventListener('click', () => {
const column = parseInt(header.dataset.column);
const isAscending = header.classList.contains('asc');
sortTable(column, !isAscending);
});
});
}

function updateMousePosition(event) {
mouseX = event.clientX;
mouseY = event.clientY;
if (activePopup) {
positionPopup(activePopup);
}
}

function showPopup(event) {
const popup = event.currentTarget.querySelector('.image-popup');
if (popup) {
activePopup = popup;
popup.style.display = 'block';
positionPopup(popup);
}
}

function hidePopup(event) {
const popup = event.currentTarget.querySelector('.image-popup');
if (popup) {
popup.style.display = 'none';
activePopup = null;
}
}

function positionPopup(popup) {
const rect = popup.getBoundingClientRect();
let left = mouseX + 10;
let top = mouseY + 10;

if (left + rect.width > window.innerWidth) {
left = window.innerWidth - rect.width - 10;
}
if (top + rect.height > window.innerHeight) {
top = window.innerHeight - rect.height - 10;
}

popup.style.left = `${left}px`;
popup.style.top = `${top}px`;
}

function handleResponsiveLayout() {
const dashboard = document.querySelector('.dashboard');
const table = document.querySelector('.content-table');
if (window.innerWidth < 1024) {
dashboard.classList.add('card-view');
table.classList.add('card-layout');
} else {
dashboard.classList.remove('card-view');
table.classList.remove('card-layout');
}
}

function filterTable() {
const filterValue = document.getElementById('status-filter').value;
const rows = document.querySelectorAll('.content-table tbody tr');
rows.forEach(row => {
const reviewDateCell = row.querySelector('.review-date-cell');
const expiryDateCell = row.querySelector('.expiry-date-cell');
const showRow = filterValue === 'all' ||
(filterValue === 'green' && reviewDateCell.classList.contains('green') && expiryDateCell.classList.contains('green')) ||
(filterValue === 'amber' && (reviewDateCell.classList.contains('amber') || expiryDateCell.classList.contains('amber'))) ||
(filterValue === 'red' && (reviewDateCell.classList.contains('red') || expiryDateCell.classList.contains('red')));
row.style.display = showRow ? '' : 'none';
});
}

function sortTable(column, ascending) {
const tbody = document.querySelector('.content-table tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));

rows.sort((a, b) => {
let aValue = a.children[column].textContent.trim();
let bValue = b.children[column].textContent.trim();

if (column >= 3) { // Date columns
aValue = new Date(aValue).getTime();
bValue = new Date(bValue).getTime();
}

if (ascending) {
return aValue > bValue ? 1 : aValue < bValue ? -1 : 0;
} else {
return aValue < bValue ? 1 : aValue > bValue ? -1 : 0;
}
});

tbody.innerHTML = '';
rows.forEach(row => tbody.appendChild(row));

const headers = document.querySelectorAll('.content-table th');
headers.forEach(header => {
header.classList.remove('asc', 'desc');
});
const sortedHeader = document.querySelector(`.content-table th[data-column="${column}"]`);
sortedHeader.classList.add(ascending ? 'asc' : 'desc');
}
}
This is the Markdown file that generates a fraction of the block named README. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/dynamic/README.md

# Dynamic block JS

This JavaScript code is part Tom Cranstoun derived variations of Helix CMS, with diagrams here <https://main--allaboutv2--ddttom.hlx.live/extra/dynamic>

It defines an asynchronous function named `decorate` that dynamically manipulates web content based on certain conditions. The function is designed to work within a specific block of a webpage, enhancing it with dynamic content fetched from a JSON file.

The file is /query-index.json which is auto-populated by Helix whenever pages are published or deleted

the dynamic pages are selected using one of three different mechanisms:

default is to use /blog/ as a path to find pages
if the current page has more than / in its path, such as /articles/index t5hen the default becomes the path
finally if there are any classnames in the helix block these are used as paths.

one can use section metadata attribute "maxReturn" to set the maximum number of entries.
if no section metadata the code looks for page metadata maxReturn
if not found it then looks for "$system:maxreturn$"
and finally it uses 8 if they do not exist.

You can pull in the headline, description, 'service' tag, 'resource' tag and url path from the query-index to the card. This will display relevant information about the article and allow a link to the page from the card.

Here's a detailed breakdown:

## ESLint Directives

- **`eslint-disable function-paren-newline, import/extensions, no-alert`**: These lines disable specific ESLint rules for this file, allowing certain code styles that ESLint would normally flag as violations. This customization is common in projects to fit the team's coding style or project's requirements.

## Imports

- **Various elements from `'../../scripts/dom-helpers.js'`**: Functions like `a`, `div`, `li`, `p`, `strong`, `ul` are imported for creating HTML elements programmatically. This approach is typical in modern web development to dynamically generate content.
- **`createOptimizedPicture` from `'../../scripts/aem.js'`**: A function designed to create optimized `<picture>` elements for responsive images, a common practice in web performance optimization.
- **`ffetch` from `'../../scripts/ffetch.js'`**: a wrapper around the Fetch API, tailored for this project's specific needs for making network requests.

### The `decorate` Function

- **Purpose**: Asynchronously fetches and processes content, then injects this content into a specified block element on the webpage.
- **Parameters**:
- `block`: The DOM element into which the dynamic content will be inserted.

#### Key Operations

1. **Element Selection**: Finds an element with the class `.dynamic-container` within the document.

2. **Max Return Calculation**: Determines the maximum number of content items to fetch, prioritizing the value of a `data-maxreturn` attribute, followed by system configurations, and defaults to `8` if none are specified.

3. **Content Fetching**: Uses the `ffetch` function to asynchronously retrieve content from `/query-index.json`.

4. **Target Names Initialization**: Sets the default content category to `blog`. It adjusts the target based on the current URL path, excluding the domain.

5. **Additional Targets**: Dynamically adjusts target names based on additional class names of the `block` element, with exclusions for specific patterns.

6. **Content Filtering**: Excludes content paths that contain `/template` or match the current page's path. Further filters the content to include only items that match the specified `targetNames`.

7. **Sorting**: Orders the filtered content by the `lastModified` timestamp in descending order.

8. **Content Injection**: Limits the displayed content based on `maxReturnNumber` and appends it to the `block` element. Each item is structured with an image, title, description, and a "Read More" link.

### Summary

The script is a sophisticated content loader that enhances user experience by dynamically displaying relevant content. It leverages modern JavaScript practices, such as asynchronous data fetching, dynamic content generation, and efficient content filtering and sorting. This approach is particularly effective in content-heavy sites, allowing for a more responsive, tailored user experience.

This is the CSS file that generates a fraction of the block named dynamic. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/dynamic/dynamic.css
.dynamic {
position: relative;
margin: 0;
}

.dynamic.blog {
background-color: var(--color-white);
box-shadow: none;
}

.dynamic.flexible-cards ul {
column-count: 1;
}

@media (width >= 700px) {
.dynamic.flexible-cards ul {
column-count: 2;
}
}

@media (width >= 900px) {
.dynamic.flexible-cards ul {
column-count: 3;
}
}

This is the JS file that generates a fraction of the block named dynamic. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/dynamic/dynamic.js
/* eslint-disable function-paren-newline */
/* eslint-disable import/extensions */
/* eslint-disable no-alert */

import { createOptimizedPicture } from '../../scripts/aem.js';
import {
a, div, li, p, h3, span, ul
} from '/plusplus/block-party/dom-helpers.js';
import ffetch from '/plusplus/block-party/ffetch.js';

export default async function decorate(block) {
// Select the element by its class
const element = document.querySelector('.dynamic-container');

// Get the value of the 'data-maxreturn' attribute, or system value, or use the default value of 8
let maxReturn = element.getAttribute('data-maxreturn') ||
window.siteConfig?.['$meta:maxreturn$'] ||
window.siteConfig?.['$system:maxreturn$'] ||
'8';

if (maxReturn === '-1') {
maxReturn = 1000;
}
const content = await ffetch('/query-index.json').all();

let targetNames = ['blog']; // Initialize targetNames with 'blog' as the default

if (!window.location.pathname.endsWith('/')) {
// Extract path segments excluding the domain
const pathSegments = window.location.pathname.split('/').filter((segment) => segment.length > 0);

// Use the pathname as target if there's more than one segment
if (pathSegments.length > 1) {
targetNames = [window.location.pathname];
}
}

// Use additional class names as targets, excluding specific class names
let bnames = block.className.replace(' block', '');
if (bnames.startsWith('dynamic')) {
bnames = bnames.replace('dynamic', '');
}
bnames = bnames.trim();
if (bnames.split(' ').length > 1) {
targetNames = bnames.split(' ');
}
// Filter content to exclude paths containing '/template' and the current page path
const filteredContent = content.filter((card) => !card.path.includes('/template') && !card.path.includes('/test') &&
card.path !== window.location.pathname && // Dynamically exclude the current page path
targetNames.some((target) => card.path.includes(`/${target}/`)),
);

// Sort the filtered content by 'lastModified' in descending order
const sortedContent = filteredContent.sort((adate, b) => {
const dateA = new Date(adate.lastModified);
const dateB = new Date(b.lastModified);
return dateB - dateA;
});

const maxReturnNumber = parseInt(maxReturn, 10);

// Append sorted and filtered content to the block, obeying limits
block.append(
ul(
...sortedContent.slice(0, maxReturnNumber).map((card) => li(
div({ class: 'card-image' },
a({ href: card.path }, // Link wrapping the image
createOptimizedPicture(card.image, card.headline, false, [{ width: '750' }]),
),
),
div({ class: 'cards-card-body' },
span({ class: 'card-tag' }, card.service),
span({ class: 'card-tag alt' }, card.resource),
h3((card.headline)),
p(card.description),
),
)),
),
);
}

This is the Markdown file that generates a fraction of the block named README. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/embed/README.md
# This block

This block is party of Adobe blocks, not included in the boilerplate; often used in projects, so added here for convenience

This is the CSS file that generates a fraction of the block named embed. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/embed/embed.css
.embed {
width: unset;
text-align: center;
max-width: 800px;
margin: 32px auto;
}

.embed > div {
display: flex;
justify-content: center;
}

.embed.embed-twitter .twitter-tweet-rendered {
margin-left: auto;
margin-right: auto;
}

.embed .embed-placeholder {
width: 100%;
aspect-ratio: 16 / 9;
position: relative;
}

.embed .embed-placeholder > * {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
inset: 0;
}

.embed .embed-placeholder picture img {
width: 100%;
height: 100%;
object-fit: cover;
}

.embed .embed-placeholder-play button {
box-sizing: border-box;
position: relative;
display: block;
transform: scale(3);
width: 22px;
height: 22px;
border: 2px solid;
border-radius: 20px;
padding: 0;
}

.embed .embed-placeholder-play button::before {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
width: 0;
height: 10px;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-left: 6px solid;
top: 4px;
left: 7px;
}

This is the JS file that generates a fraction of the block named embed. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/embed/embed.js
/*
* Embed Block
* Show videos and social posts directly on your page
* https://www.hlx.live/developer/block-collection/embed
*/

const loadScript = (url, callback, type) => {
const head = document.querySelector('head');
const script = document.createElement('script');
script.src = url;
if (type) {
script.setAttribute('type', type);
}
script.onload = callback;
head.append(script);
return script;
};

const getDefaultEmbed = (url) => `<div style="left: 0; width: 100%; height: 0; position: relative; padding-bottom: 56.25%;">
<iframe src="${url.href}" style="border: 0; top: 0; left: 0; width: 100%; height: 100%; position: absolute;" allowfullscreen=""
scrolling="no" allow="encrypted-media" title="Content from ${url.hostname}" loading="lazy">
</iframe>
</div>`;

const embedYoutube = (url, autoplay) => {
const usp = new URLSearchParams(url.search);
const suffix = autoplay ? '&muted=1&autoplay=1' : '';
let vid = usp.get('v') ? encodeURIComponent(usp.get('v')) : '';
const embed = url.pathname;
if (url.origin.includes('youtu.be')) {
[, vid] = url.pathname.split('/');
}
const embedHTML = `<div style="left: 0; width: 100%; height: 0; position: relative; padding-bottom: 56.25%;">
<iframe src="https://www.youtube.com${vid ? `/embed/${vid}?rel=0&v=${vid}${suffix}` : embed}" style="border: 0; top: 0; left: 0; width: 100%; height: 100%; position: absolute;"
allow="autoplay; fullscreen; picture-in-picture; encrypted-media; accelerometer; gyroscope; picture-in-picture" allowfullscreen="" scrolling="no" title="Content from Youtube" loading="lazy"></iframe>
</div>`;
return embedHTML;
};

const embedVimeo = (url, autoplay) => {
const [, video] = url.pathname.split('/');
const suffix = autoplay ? '?muted=1&autoplay=1' : '';
const embedHTML = `<div style="left: 0; width: 100%; height: 0; position: relative; padding-bottom: 56.25%;">
<iframe src="https://player.vimeo.com/video/${video}${suffix}"
style="border: 0; top: 0; left: 0; width: 100%; height: 100%; position: absolute;"
frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen
title="Content from Vimeo" loading="lazy"></iframe>
</div>`;
return embedHTML;
};

const embedTwitter = (url) => {
const embedHTML = `<blockquote class="twitter-tweet"><a href="${url.href}"></a></blockquote>`;
loadScript('https://platform.twitter.com/widgets.js');
return embedHTML;
};

const loadEmbed = (block, link, autoplay) => {
if (block.classList.contains('embed-is-loaded')) {
return;
}

const EMBEDS_CONFIG = [
{
match: ['youtube', 'youtu.be'],
embed: embedYoutube,
},
{
match: ['vimeo'],
embed: embedVimeo,
},
{
match: ['twitter'],
embed: embedTwitter,
},
];

const config = EMBEDS_CONFIG.find((e) => e.match.some((match) => link.includes(match)));
const url = new URL(link);
if (config) {
block.innerHTML = config.embed(url, autoplay);
block.classList = `block embed embed-${config.match[0]}`;
} else {
block.innerHTML = getDefaultEmbed(url);
block.classList = 'block embed';
}
block.classList.add('embed-is-loaded');
};

export default function decorate(block) {
const placeholder = block.querySelector('picture');
const link = block.querySelector('a').href;
block.textContent = '';

if (placeholder) {
const wrapper = document.createElement('div');
wrapper.className = 'embed-placeholder';
wrapper.innerHTML = '<div class="embed-placeholder-play"><button type="button" title="Play"></button></div>';
wrapper.prepend(placeholder);
wrapper.addEventListener('click', () => {
loadEmbed(block, link, true);
});
block.append(wrapper);
} else {
const observer = new IntersectionObserver((entries) => {
if (entries.some((e) => e.isIntersecting)) {
observer.disconnect();
loadEmbed(block, link);
}
});
observer.observe(block);
}
}

This is the CSS file that generates a fraction of the block named footer. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/footer/footer.css
footer {
padding: 2rem;
background-color: var(--light-color);
font-size: var(--body-font-size-s);
}

footer .footer {
max-width: 1200px;
margin: auto;
}

footer .footer p {
margin: 0;
}

This is the JS file that generates a fraction of the block named footer. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/footer/footer.js
import { getMetadata } from '../../scripts/aem.js';
import { loadFragment } from '../fragment/fragment.js';

/**
* loads and decorates the footer
* @param {Element} block The footer block element
*/
export default async function decorate(block) {
const footerMeta = getMetadata('footer');
block.textContent = '';

// load footer fragment
const footerPath = footerMeta.footer || '/footer';
const fragment = await loadFragment(footerPath);

// decorate footer DOM
const footer = document.createElement('div');
while (fragment.firstElementChild) footer.append(fragment.firstElementChild);

block.append(footer);
}

This is the CSS file that generates a fraction of the block named fortunecookie. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/fortunecookie/fortunecookie.css

This is the JS file that generates a fraction of the block named fortunecookie. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/fortunecookie/fortunecookie.js

export default async function decorate(block) {
const fortuneCookieElement = document.querySelector('.fortunecookie');
const url = '/data/cookies.json';
try {
const response = await fetch(url);
const data = await response.json();

const dataArray = data.data;
const randomIndex = Math.floor(Math.random() * dataArray.length);
const randomItem = dataArray[randomIndex];

const content = `<p><strong>${randomItem.key}:</strong> ${randomItem.value}</p>`;
fortuneCookieElement.innerHTML = content;
} catch (error) {
console.error('Error fetching the JSON data:', error);
}
}
This is the Markdown file that generates a fraction of the block named README. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/fragment/README.md
# This block

This block is party of Adobe blocks, not included in the boilerplate; often used in projects, so added here for convenience

This is the CSS file that generates a fraction of the block named fragment. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/fragment/fragment.css
/* suppress nested section padding */
.fragment-wrapper > .section {
padding-left: 0;
padding-right: 0;
}

.fragment-wrapper > .section:first-of-type {
padding-top: 0;
}

.fragment-wrapper > .section:last-of-type {
padding-bottom: 0;
}

This is the JS file that generates a fraction of the block named fragment. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/fragment/fragment.js
/*
* Fragment Block
* Include content on a page as a fragment.
* https://www.aem.live/developer/block-collection/fragment
*/

import {
decorateMain,
} from '../../scripts/scripts.js';

import {
loadBlocks,
} from '../../scripts/aem.js';

/**
* Loads a fragment.
* @param {string} path The path to the fragment
* @returns {HTMLElement} The root element of the fragment
*/
export async function loadFragment(path) {
if (path && path.startsWith('/')) {
const resp = await fetch(`${path}.plain.html`);
if (resp.ok) {
const main = document.createElement('main');
main.innerHTML = await resp.text();

// reset base path for media to fragment base
const resetAttributeBase = (tag, attr) => {
main.querySelectorAll(`${tag}[${attr}^="./media_"]`).forEach((elem) => {
elem[attr] = new URL(elem.getAttribute(attr), new URL(path, window.location)).href;
});
};
resetAttributeBase('img', 'src');
resetAttributeBase('source', 'srcset');

decorateMain(main);
await loadBlocks(main);
return main;
}
}
return null;
}

export default async function decorate(block) {
const link = block.querySelector('a');
const path = link ? link.getAttribute('href') : block.textContent.trim();
const fragment = await loadFragment(path);
if (fragment) {
const fragmentSection = fragment.querySelector(':scope .section');
if (fragmentSection) {
block.closest('.section').classList.add(...fragmentSection.classList);
block.closest('.fragment').replaceWith(...fragment.childNodes);
}
}
}

This is the CSS file that generates a fraction of the block named grid. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/grid/grid.css

/* stylelint-disable */
/* Generated File: Do not edit directly */
.grid{grid-gap:var(--ros-semantic-spacing-gap-component-lg);display:grid;gap:var(--ros-semantic-spacing-gap-component-lg)}.grid.two-cols{display:grid;grid-template-columns:repeat(1,minmax(0,1fr))}@media (min-width:992px){.grid.two-cols{grid-template-columns:repeat(2,minmax(0,1fr))}}.grid.bulleted>div{display:flex}.grid.bulleted>div .icon{display:flex;flex:0 0 8.33333%;max-width:100%;max-width:8.33333%;min-height:1px;padding-left:var(--ros-semantic-spacing-around-component-xs);padding-right:var(--ros-semantic-spacing-around-component-xs);padding-top:var(--ros-semantic-spacing-around-component-xs);position:relative;width:100%}.section[\:has\(\.img-grid\)]{border-top:1px solid var(--ros-semantic-color-seperator-on-base);min-height:98px;padding:var(--ros-semantic-spacing-around-component-xl) var(--ros-semantic-spacing-around-component-md)}.section:has(.img-grid){border-top:1px solid var(--ros-semantic-color-seperator-on-base);min-height:98px;padding:var(--ros-semantic-spacing-around-component-xl) var(--ros-semantic-spacing-around-component-md)}.logo-grid{display:grid;grid-template-columns:repeat(1,minmax(0,1fr))}@media (min-width:576px){.logo-grid{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:768px){.logo-grid{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:992px){.logo-grid{grid-template-columns:repeat(5,minmax(0,1fr))}}.logo-grid>div{display:flex;height:57px;justify-content:center}.logo-grid a{all:unset}.logo-grid img{max-height:50px;width:auto}.section[\:has\(\.logo-grid\)]{border-bottom:var(--ros-semantic-border-width-sm) solid var(--ros-semantic-color-seperator-on-base);border-top:var(--ros-semantic-border-width-sm) solid var(--ros-semantic-color-seperator-on-base);min-height:98px;padding-bottom:var(--ros-semantic-spacing-around-component-xl);padding-top:var(--ros-semantic-spacing-around-component-xl)}.section:has(.logo-grid){border-bottom:var(--ros-semantic-border-width-sm) solid var(--ros-semantic-color-seperator-on-base);border-top:var(--ros-semantic-border-width-sm) solid var(--ros-semantic-color-seperator-on-base);min-height:98px;padding-bottom:var(--ros-semantic-spacing-around-component-xl);padding-top:var(--ros-semantic-spacing-around-component-xl)}.icon-cards{display:grid;grid-template-columns:repeat(1,minmax(0,1fr))}@media (min-width:576px){.icon-cards{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:768px){.icon-cards{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:992px){.icon-cards{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:1200px){.icon-cards{grid-template-columns:repeat(4,minmax(0,1fr))}}.icon-cards{gap:var(--ros-semantic-spacing-gap-component-xl);text-align:center}.icon-cards .h1,.icon-cards .h2,.icon-cards .h3,.icon-cards .h4,.icon-cards .h5,.icon-cards h1,.icon-cards h2,.icon-cards h3,.icon-cards h4,.icon-cards h5{--fluid-min-width:360;--fluid-max-width:1200;--fluid-screen:100vw;--fluid-bp:calc((var(--fluid-screen) - var(--fluid-min-width)/16*1rem)/(var(--fluid-max-width) - var(--fluid-min-width)));font-size:calc((var(--min)/16)*1rem + (var(--max) - var(--min))*var(--fluid-bp))}@media (min-width:1200px){.icon-cards .h1,.icon-cards .h2,.icon-cards .h3,.icon-cards .h4,.icon-cards .h5,.icon-cards h1,.icon-cards h2,.icon-cards h3,.icon-cards h4,.icon-cards h5{--fluid-screen:calc(var(--fluid-max-width)*1px)}}.icon-cards .h1,.icon-cards .h2,.icon-cards .h3,.icon-cards .h4,.icon-cards .h5,.icon-cards h1,.icon-cards h2,.icon-cards h3,.icon-cards h4,.icon-cards h5{font-family:var(--ros-semantic-font-family-heading);font-weight:var(--ros-semantic-font-weight-heading-regular);line-height:var(--ros-semantic-line-height-heading);margin-bottom:var(--ros-semantic-spacing-typography-heading-margin-bottom);margin-top:0;-webkit-text-decoration:var(--ros-semantic-text-decoration-heading);text-decoration:var(--ros-semantic-text-decoration-heading);text-transform:var(--ros-semantic-text-case-heading)}.icon-cards .h1 .bold,.icon-cards .h1 b,.icon-cards .h2 .bold,.icon-cards .h2 b,.icon-cards .h3 .bold,.icon-cards .h3 b,.icon-cards .h4 .bold,.icon-cards .h4 b,.icon-cards .h5 .bold,.icon-cards .h5 b,.icon-cards h1 .bold,.icon-cards h1 b,.icon-cards h2 .bold,.icon-cards h2 b,.icon-cards h3 .bold,.icon-cards h3 b,.icon-cards h4 .bold,.icon-cards h4 b,.icon-cards h5 .bold,.icon-cards h5 b{font-weight:var(--ros-semantic-font-weight-heading-bold)}.icon-cards .h1,.icon-cards .h2,.icon-cards .h3,.icon-cards .h4,.icon-cards .h5,.icon-cards h1,.icon-cards h2,.icon-cards h3,.icon-cards h4,.icon-cards h5{--min:var(--ros-semantic-font-size-mobile-heading-5);--max:var(--ros-semantic-font-size-desktop-heading-5);font-weight:var(--ros-semantic-font-weight-semi-bold)}.icon-cards svg{color:default-default;color:var(--ros-semantic-color-foreground-on-layer-2 default-default);height:var(--ros-semantic-size-icon-xl-height);transform:rotate(-45deg);width:var(--ros-semantic-size-icon-xl-width)}.icon-cards .icon{align-items:center;background-color:var(--ros-semantic-color-background-layer-2);border-radius:10px;display:flex;height:60px;justify-items:center;margin:0 auto;opacity:.7;padding:var(--ros-semantic-spacing-around-component-xl);position:relative;transform:rotate(45deg);width:60px}.icon-cards>div{border-radius:var(--ros-semantic-border-radius-default);box-shadow:var(--ros-semantic-drop-shadow-md-0-x) var(--ros-semantic-drop-shadow-md-0-y) var(--ros-semantic-drop-shadow-md-0-blur) var(--ros-semantic-drop-shadow-md-0-spread) var(--ros-semantic-drop-shadow-md-0-color),var(--ros-semantic-drop-shadow-md-1-x) var(--ros-semantic-drop-shadow-md-1-y) var(--ros-semantic-drop-shadow-md-1-blur) var(--ros-semantic-drop-shadow-md-1-spread) var(--ros-semantic-drop-shadow-md-1-color);padding:var(--ros-semantic-spacing-around-component-3xl)}.icon-cards>div p[\:not-has\(\.icon\)]{margin-bottom:0}.icon-cards>div p:not(:has(.icon)){margin-bottom:0}.icon-cards>div:hover{background-color:var(--ros-semantic-color-background-primary);color:var(--ros-semantic-color-foreground-on-on-primary-default)}
This is the JS file that generates a fraction of the block named grid. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/grid/grid.js

This is the CSS file that generates a fraction of the block named header. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/header/header.css
/* header and nav layout */
header .nav-wrapper {
background-color: var(--background-color);
width: 100%;
z-index: 2;
position: fixed;
}

header nav {
box-sizing: border-box;
display: grid;
grid-template:
'hamburger brand tools' var(--nav-height)
'sections sections sections' 1fr / auto 1fr auto;
align-items: center;
gap: 0 2em;
margin: auto;
max-width: 1264px;
height: var(--nav-height);
padding: 0 1rem;
font-family: var(--body-font-family);
}

header nav[aria-expanded="true"] {
grid-template:
'hamburger brand' var(--nav-height)
'sections sections' 1fr
'tools tools' var(--nav-height) / auto 1fr;
overflow-y: auto;
min-height: 100vh;
}

@media (width >= 600px) {
header nav {
padding: 0 2rem;
}
}

@media (width >= 900px) {
header nav {
display: flex;
justify-content: space-between;
}

header nav[aria-expanded="true"] {
min-height: 0;
overflow: visible;
}
}

header nav p {
margin: 0;
line-height: 1;
}

header nav a:any-link {
color: currentcolor;
}

/* hamburger */
header nav .nav-hamburger {
grid-area: hamburger;
height: 22px;
display: flex;
align-items: center;
}

header nav .nav-hamburger button {
height: 22px;
margin: 0;
border: 0;
border-radius: 0;
padding: 0;
background-color: var(--background-color);
color: inherit;
overflow: initial;
text-overflow: initial;
white-space: initial;
}

header nav .nav-hamburger-icon,
header nav .nav-hamburger-icon::before,
header nav .nav-hamburger-icon::after {
box-sizing: border-box;
display: block;
position: relative;
width: 20px;
}

header nav .nav-hamburger-icon::before,
header nav .nav-hamburger-icon::after {
content: '';
position: absolute;
background: currentcolor;
}

header nav[aria-expanded="false"] .nav-hamburger-icon,
header nav[aria-expanded="false"] .nav-hamburger-icon::before,
header nav[aria-expanded="false"] .nav-hamburger-icon::after {
height: 2px;
border-radius: 2px;
background: currentcolor;
}

header nav[aria-expanded="false"] .nav-hamburger-icon::before {
top: -6px;
}

header nav[aria-expanded="false"] .nav-hamburger-icon::after {
top: 6px;
}

header nav[aria-expanded="true"] .nav-hamburger-icon {
height: 22px;
}

header nav[aria-expanded="true"] .nav-hamburger-icon::before,
header nav[aria-expanded="true"] .nav-hamburger-icon::after {
top: 3px;
left: 1px;
transform: rotate(45deg);
transform-origin: 2px 1px;
width: 24px;
height: 2px;
border-radius: 2px;
}

header nav[aria-expanded="true"] .nav-hamburger-icon::after {
top: unset;
bottom: 3px;
transform: rotate(-45deg);
}

@media (width >= 900px) {
header nav .nav-hamburger {
display: none;
visibility: hidden;
}
}

/* brand */
header .nav-brand {
grid-area: brand;
flex-basis: 128px;
font-size: var(--heading-font-size-s);
font-weight: 700;
line-height: 1;
}

header nav .nav-brand img {
width: 128px;
height: auto;
}

/* sections */
header nav .nav-sections {
grid-area: sections;
flex: 1 1 auto;
display: none;
visibility: hidden;
background-color: var(--overlay-color);
}

header nav[aria-expanded="true"] .nav-sections {
display: block;
visibility: visible;
align-self: start;
}

header nav .nav-sections ul {
list-style: none;
padding-left: 0;
font-size: var(--body-font-size-s);
font-weight: 500;
}

header nav .nav-sections ul > li {
font-weight: 700;
}

header nav .nav-sections ul > li > ul {
margin-top: 0;
}

header nav .nav-sections ul > li > ul > li {
font-weight: 500;
}

@media (width >= 900px) {
header nav .nav-sections {
display: block;
visibility: visible;
white-space: nowrap;
}

header nav[aria-expanded="true"] .nav-sections {
align-self: unset;
}

header nav .nav-sections .nav-drop {
position: relative;
padding-right: 16px;
cursor: pointer;
}

header nav .nav-sections .nav-drop::after {
content: '';
display: inline-block;
position: absolute;
top: .5em;
right: 2px;
transform: rotate(135deg);
width: 6px;
height: 6px;
border: 2px solid currentcolor;
border-radius: 0 1px 0 0;
border-width: 2px 2px 0 0;
}

header nav .nav-sections .nav-drop[aria-expanded="true"]::after {
top: unset;
bottom: .5em;
transform: rotate(315deg);
}

header nav .nav-sections ul {
display: flex;
gap: 2em;
margin: 0;
font-size: var(--body-font-size-xs);
}

header nav .nav-sections .default-content-wrapper > ul > li {
flex: 0 1 auto;
position: relative;
font-weight: 500;
}

header nav .nav-sections .default-content-wrapper > ul > li > ul {
display: none;
position: relative;
}

header nav .nav-sections .default-content-wrapper > ul > li[aria-expanded="true"] > ul {
display: block;
position: absolute;
left: -1em;
width: 200px;
margin-top: 12px;
padding: 1em;
background-color: var(--light-color);
white-space: initial;
}

header nav .nav-sections .default-content-wrapper > ul > li > ul::before {
content: '';
position: absolute;
top: -8px;
left: 8px;
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-bottom: 8px solid var(--light-color);
}

header nav .nav-sections .default-content-wrapper > ul > li > ul > li {
padding: 8px 0;
}
}

/* tools */
header nav .nav-tools {
grid-area: tools;
}

This is the JS file that generates a fraction of the block named header. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/header/header.js
import { getMetadata } from '../../scripts/aem.js';
import { loadFragment } from '../fragment/fragment.js';

// media query match that indicates mobile/tablet width
const isDesktop = window.matchMedia('(min-width: 900px)');

function closeOnEscape(e) {
if (e.code === 'Escape') {
const nav = document.getElementById('nav');
const navSections = nav.querySelector('.nav-sections');
const navSectionExpanded = navSections.querySelector('[aria-expanded="true"]');
if (navSectionExpanded && isDesktop.matches) {
// eslint-disable-next-line no-use-before-define
toggleAllNavSections(navSections);
navSectionExpanded.focus();
} else if (!isDesktop.matches) {
// eslint-disable-next-line no-use-before-define
toggleMenu(nav, navSections);
nav.querySelector('button').focus();
}
}
}

function openOnKeydown(e) {
const focused = document.activeElement;
const isNavDrop = focused.className === 'nav-drop';
if (isNavDrop && (e.code === 'Enter' || e.code === 'Space')) {
const dropExpanded = focused.getAttribute('aria-expanded') === 'true';
// eslint-disable-next-line no-use-before-define
toggleAllNavSections(focused.closest('.nav-sections'));
focused.setAttribute('aria-expanded', dropExpanded ? 'false' : 'true');
}
}

function focusNavSection() {
document.activeElement.addEventListener('keydown', openOnKeydown);
}

/**
* Toggles all nav sections
* @param {Element} sections The container element
* @param {Boolean} expanded Whether the element should be expanded or collapsed
*/
function toggleAllNavSections(sections, expanded = false) {
sections.querySelectorAll('.nav-sections .default-content-wrapper > ul > li').forEach((section) => {
section.setAttribute('aria-expanded', expanded);
});
}

/**
* Toggles the entire nav
* @param {Element} nav The container element
* @param {Element} navSections The nav sections within the container element
* @param {*} forceExpanded Optional param to force nav expand behavior when not null
*/
function toggleMenu(nav, navSections, forceExpanded = null) {
const expanded = forceExpanded !== null ? !forceExpanded : nav.getAttribute('aria-expanded') === 'true';
const button = nav.querySelector('.nav-hamburger button');
document.body.style.overflowY = (expanded || isDesktop.matches) ? '' : 'hidden';
nav.setAttribute('aria-expanded', expanded ? 'false' : 'true');
toggleAllNavSections(navSections, expanded || isDesktop.matches ? 'false' : 'true');
button.setAttribute('aria-label', expanded ? 'Open navigation' : 'Close navigation');
// enable nav dropdown keyboard accessibility
const navDrops = navSections.querySelectorAll('.nav-drop');
if (isDesktop.matches) {
navDrops.forEach((drop) => {
if (!drop.hasAttribute('tabindex')) {
drop.setAttribute('role', 'button');
drop.setAttribute('tabindex', 0);
drop.addEventListener('focus', focusNavSection);
}
});
} else {
navDrops.forEach((drop) => {
drop.removeAttribute('role');
drop.removeAttribute('tabindex');
drop.removeEventListener('focus', focusNavSection);
});
}
// enable menu collapse on escape keypress
if (!expanded || isDesktop.matches) {
// collapse menu on escape press
window.addEventListener('keydown', closeOnEscape);
} else {
window.removeEventListener('keydown', closeOnEscape);
}
}

/**
* decorates the header, mainly the nav
* @param {Element} block The header block element
*/
export default async function decorate(block) {
// load nav as fragment
const navMeta = getMetadata('nav');
const navPath = navMeta ? new URL(navMeta).pathname : '/nav';
const fragment = await loadFragment(navPath);

// decorate nav DOM
const nav = document.createElement('nav');
nav.id = 'nav';
while (fragment.firstElementChild) nav.append(fragment.firstElementChild);

const classes = ['brand', 'sections', 'tools'];
classes.forEach((c, i) => {
const section = nav.children[i];
if (section) section.classList.add(`nav-${c}`);
});

const navBrand = nav.querySelector('.nav-brand');
const brandLink = navBrand.querySelector('.button');
if (brandLink) {
brandLink.className = '';
brandLink.closest('.button-container').className = '';
}

const navSections = nav.querySelector('.nav-sections');
if (navSections) {
navSections.querySelectorAll(':scope .default-content-wrapper > ul > li').forEach((navSection) => {
if (navSection.querySelector('ul')) navSection.classList.add('nav-drop');
navSection.addEventListener('click', () => {
if (isDesktop.matches) {
const expanded = navSection.getAttribute('aria-expanded') === 'true';
toggleAllNavSections(navSections);
navSection.setAttribute('aria-expanded', expanded ? 'false' : 'true');
}
});
});
// hamburger for mobile
const hamburger = document.createElement('div');
hamburger.classList.add('nav-hamburger');
hamburger.innerHTML = `<button type="button" aria-controls="nav" aria-label="Open navigation">
<span class="nav-hamburger-icon"></span>
</button>`;
hamburger.addEventListener('click', () => toggleMenu(nav, navSections));
nav.prepend(hamburger);
nav.setAttribute('aria-expanded', 'false');
// prevent mobile nav behavior on window resize
toggleMenu(nav, navSections, isDesktop.matches);
isDesktop.addEventListener('change', () => toggleMenu(nav, navSections, isDesktop.matches));

const navWrapper = document.createElement('div');
navWrapper.className = 'nav-wrapper';
navWrapper.append(nav);
block.append(navWrapper);
}
}

This is the Markdown file that generates a fraction of the block named README. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/helloworld/README.md
# This block

This block is the normal'hello world' used to introduce any developer to a new language/platform

This is the CSS file that generates a fraction of the block named helloworld. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/helloworld/helloworld.css
.helloworld {
background-color: #f0f0f0;
padding: 20px;
border-radius: 5px;
text-align: center;
font-size: 24px;
font-weight: bold;
color: #333;
}
This is the JS file that generates a fraction of the block named helloworld. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/helloworld/helloworld.js
export default function decorate(block) {
const greeting = document.createElement('div');
greeting.textContent = 'Hello World';
block.textContent = '';
block.appendChild(greeting);
}
This is the CSS file that generates a fraction of the block named hero. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/hero/hero.css
/* Block specific CSS */

/* Hero container styles */
main .hero-container > div {
max-width: unset;
}

main .hero-container {
padding: 0;
}

/* General hero styles */
main .hero {
position: relative;
padding: 32px;
min-height: 1000px;
display: flex;
justify-content: flex-start; /* Align items to the start horizontally */
align-items: center; /* Center items vertically */
text-align: left; /* Align text to the left */
}

/* Hero picture styles */
main .hero picture {
position: absolute;
z-index: -1;
top: 0;
left: 0;
bottom: 0;
right: 0;
object-fit: cover;
box-sizing: border-box;
}

/* Hero image styles */
main .hero img {
object-fit: cover;
width: 100%;
height: 100%;
}
body.techem .hero-wrapper, .hero-wrapper * {
color: white;
}

body.techem hero-wrapper a {
color: white;
}

/* Styles for .hero.manual */
.hero-wrapper .hero.manual {
color: white; /* Text color */
padding-top: 100px; /* Top padding */
/* Additional styles for .hero.Manual */
}

.hero-wrapper .hero.manual > div:last-child h2 {
line-height: 69px;
margin: 0;
padding-inline: 0;
}

/* Additional general hero styles */
.section .hero-wrapper {
max-width: 120rem;
margin-top: 5rem;
margin-inline: auto;
}
.hero-wrapper .hero {
display: flex;
justify-content: center;
align-items: center;
height: 32.5rem;
min-height: auto;
}

.hero-wrapper .hero > div:last-child h2 {
line-height: 69px;
margin: 0;
padding-inline: 0.5rem;
}

.hero.embolden{
font-weight: bolder;
}

.hero.techem {
text-align: center;
}

.hero.techem > div > div {
max-width: 77pc;
margin-inline: auto;
}

.hero.techem h3 {
font-size: 3rem;
line-height: 4rem;
margin-bottom: 0;
}
This is the JS file that generates a fraction of the block named hero. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/hero/hero.js
export default function decorate(block) {
const firstPicture = document.querySelector('.hero > div:first-of-type picture');
const secondPicture = document.querySelector('.hero > div:first-of-type > div:nth-of-type(2) picture');

if (firstPicture && secondPicture) {
// Select the second source element from the second picture element
const secondSource = secondPicture.querySelector('source:nth-of-type(2)');
if (secondSource) {
const newSource = secondSource.cloneNode(true);
const firstPictureSecondSource = firstPicture.querySelector('source:nth-of-type(2)');
if (firstPictureSecondSource) {
firstPicture.replaceChild(newSource, firstPictureSecondSource);
} else {
firstPicture.appendChild(newSource);
}
secondPicture.remove();
}
}
}

This is the Markdown file that generates a fraction of the block named README. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/index/README.md
# index block

This code builds an index based on the h1, h2, etc

This is the CSS file that generates a fraction of the block named index. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/index/index.css

.index {
background-color: #f5f5f5;
border: 1px solid #ccc;
border-radius: 4px;
padding: 10px;
cursor: pointer;
}

.index-header {
display: flex;
align-items: center;
justify-content: space-between;
}

.arrow {
border: solid black;
border-width: 0 2px 2px 0;
display: inline-block;
padding: 3px;
transition: transform 0.3s;
}

.down {
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
}

.index-content {
display: none;
margin-top: 10px;
}

.index-content ul {
list-style-type: none;
padding: 0;
}

.index-content ul li {
margin-bottom: 5px;
}

.index-content ul li a {
text-decoration: none;
color: #333;
}
This is the JS file that generates a fraction of the block named index. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/index/index.js
/* eslint-disable no-unused-vars */
/* eslint-disable no-use-before-define */
export default function decorate(block) {
const headers = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
const indexBlock = document.querySelector('.index');
// Create the index header
const indexHeader = document.createElement('div');
indexHeader.className = 'index-header';
indexHeader.innerHTML = `
<span>Index</span>
<i class='arrow down'></i>
`;
// Create the index content container
const indexContent = document.createElement('div');
indexContent.className = 'index-content';
// Append the index header and content container to the index block
indexBlock.appendChild(indexHeader);
indexBlock.appendChild(indexContent);

let isIndexBuilt = false; // Flag to track if the index has been built

indexHeader.addEventListener('click', () => {
if (!isIndexBuilt) {
buildIndex();
isIndexBuilt = true; // Set the flag to true after building the index
indexContent.style.display = 'none';
}

if (indexContent.style.display === 'none') {
indexContent.style.display = 'block';
indexHeader.querySelector('.arrow').style.transform = 'rotate(-135deg)';
} else {
indexContent.style.display = 'none';
indexHeader.querySelector('.arrow').style.transform = 'rotate(45deg)';
}
});

function buildIndex() {
const indexContent2 = document.querySelector('.index-content');
const ul = document.createElement('ul');
headers.forEach((header, index) => {
const id = `header-${index}`;
header.id = id;
const li = document.createElement('li');
li.style.marginLeft = `${(parseInt(header.tagName[1], 10) - 1) * 20}px`;
const a = document.createElement('a');
a.href = `#${id}`;
a.textContent = header.textContent;
li.appendChild(a);
ul.appendChild(li);
});
indexContent2.innerHTML = '';
indexContent2.appendChild(ul);
}
}

This is the Markdown file that generates a fraction of the block named README. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/quote/README.md
# This block

This block is party of Adobe blocks, not included in the boilerplate; often used in projects, so added here for convenience

This is the CSS file that generates a fraction of the block named quote. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/quote/quote.css
.quote blockquote {
margin: 0 auto;
padding: 0 32px;
max-width: 700px;
}

.quote blockquote .quote-quotation {
font-size: 120%;
}

.quote blockquote .quote-quotation > :first-child {
text-indent: -0.6ch;
}

.quote blockquote .quote-quotation > :before,
.quote blockquote .quote-quotation > :after {
line-height: 0;
}

.quote blockquote .quote-quotation > :before {
content: "“";
}

.quote blockquote .quote-quotation > :after {
content: "”";
}

.quote blockquote .quote-attribution {
text-align: right;
}

.quote blockquote .quote-attribution > :before {
content: "—";
}

This is the JS file that generates a fraction of the block named quote. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/quote/quote.js
function hasWrapper(el) {
return !!el.firstElementChild && window.getComputedStyle(el.firstElementChild).display === 'block';
}

export default async function decorate(block) {
const [quotation, attribution] = [...block.children].map((c) => c.firstElementChild);
const blockquote = document.createElement('blockquote');
// decorate quotation
quotation.className = 'quote-quotation';
if (!hasWrapper(quotation)) {
quotation.innerHTML = `<p>${quotation.innerHTML}</p>`;
}
blockquote.append(quotation);
// decoration attribution
if (attribution) {
attribution.className = 'quote-attribution';
if (!hasWrapper(attribution)) {
attribution.innerHTML = `<p>${attribution.innerHTML}</p>`;
}
blockquote.append(attribution);
const ems = attribution.querySelectorAll('em');
ems.forEach((em) => {
const cite = document.createElement('cite');
cite.innerHTML = em.innerHTML;
em.replaceWith(cite);
});
}
block.innerHTML = '';
block.append(blockquote);
}

This is the Markdown file that generates a fraction of the block named README. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/returntotop/README.md
# return to top block

This code adds a "return to top" button feature to the webpage. Here's how it works:

1. **Button Selection:** It first identifies the button element on the webpage that is intended to return the user to the top of the page. This button is marked with a specific class.

2. **Scroll Event Handling:** The code listens for when the user scrolls up or down on the page. If the user scrolls down more than 100 pixels, the button is made visible. If the user scrolls back up so that they are within 100 pixels from the top, the button is hidden.

3. **Button Click Behavior:** When the visible button is clicked, the page smoothly scrolls back to the top. This gives users a quick and easy way to return to the top of the page without having to manually scroll back up.

This is the CSS file that generates a fraction of the block named returntotop. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/returntotop/returntotop.css
.returntotop {
position: fixed;
bottom: 20px;
left: 20px;
padding: 10px 20px;
background-color: #007BFF;
color: white;
cursor: pointer;
text-align: center;
border-radius: 5px;
text-decoration: none;
font-size: 14px;
display: none;
}

.returntotop:hover {
background-color: #0056b3;
}
This is the JS file that generates a fraction of the block named returntotop. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/returntotop/returntotop.js
// eslint-disable-next-line no-unused-vars
export default async function decorate(block) {
const returnToTopButton = document.querySelector('.returntotop');
window.addEventListener('scroll', () => {
if (window.scrollY > 100) {
returnToTopButton.style.display = 'block';
} else {
returnToTopButton.style.display = 'none';
}
});

// Scroll to top when the button is clicked
returnToTopButton.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
});
}

This is the Markdown file that generates a fraction of the block named README. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/search/README.md
# This block

This block is party of Adobe blocks, not included in the boilerplate; often used in projects, so added here for convenience

This is the CSS file that generates a fraction of the block named search. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/search/search.css
/* search box */
.search .search-box {
display: grid;
grid-template-columns: 24px 1fr;
gap: 8px;
align-items: center;
}

.search .search-box input {
width: 100%;
border: 1px solid var(--dark-color);
padding: 5px 15px;
font: inherit;
}

/* search results */
.search ul.search-results {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(278px, 1fr));
gap: 16px;
padding-left: 0;
list-style: none;
}

.search ul.search-results > li {
border: 1px solid var(--dark-color);
}

.search ul.search-results > li > a {
display: block;
background-color: var(--background-color);
color: currentcolor;
cursor: pointer;
height: 100%;
}

.search ul.search-results > li > a:hover,
.search ul.search-results > li > a:focus {
text-decoration: none;
}

.search ul.search-results > li .search-result-title,
.search ul.search-results > li p {
padding: 0 16px;
}

.search ul.search-results > li .search-result-title {
font-size: var(--body-font-size-m);
font-weight: normal;
}

.search ul.search-results > li .search-result-title a {
color: currentcolor;
text-decoration: none;
}

.search ul.search-results > li p {
font-size: var(--body-font-size-s);
}

.search ul.search-results > li .search-result-image {
aspect-ratio: 4 / 3;
}

.search ul.search-results > li picture img {
display: block;
width: 100%;
object-fit: cover;
}

/* no results */
.search ul.search-results.no-results {
display: block;
padding-left: 32px;
}

.search ul.search-results.no-results > li {
border: none;
}

/* minimal variant */
.search.minimal ul.search-results {
display: block;
padding-left: 32px;
}

.search.minimal ul.search-results > li {
position: relative;
border: none;
}

.search.minimal ul.search-results > li .search-result-title,
.search.minimal ul.search-results > li p {
padding: unset;
}

.search.minimal ul.search-results > li .search-result-title a {
color: var(--link-color);
}

/* stylelint-disable no-descending-specificity */
.search.minimal ul.search-results > li > a {
background-color: unset;
}

.search.minimal ul.search-results > li > a:hover a,
.search.minimal ul.search-results > li > a:focus a {
text-decoration: underline;
color: var(--link-hover-color);
}

.search.minimal ul.search-results > li .search-result-image {
position: absolute;
top: 2px;
left: -32px;
}

.search.minimal ul.search-results > li picture img {
height: 24px;
width: 24px;
border-radius: 50%;
}

This is the JS file that generates a fraction of the block named search. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/search/search.js
import {
createOptimizedPicture,
decorateIcons,
fetchPlaceholders,
} from '../../scripts/aem.js';

const searchParams = new URLSearchParams(window.location.search);

function findNextHeading(el) {
let preceedingEl = el.parentElement.previousElement || el.parentElement.parentElement;
let h = 'H2';
while (preceedingEl) {
const lastHeading = [...preceedingEl.querySelectorAll('h1, h2, h3, h4, h5, h6')].pop();
if (lastHeading) {
const level = parseInt(lastHeading.nodeName[1], 10);
h = level < 6 ? `H${level + 1}` : 'H6';
preceedingEl = false;
} else {
preceedingEl = preceedingEl.previousElement || preceedingEl.parentElement;
}
}
return h;
}

function highlightTextElements(terms, elements) {
elements.forEach((element) => {
if (!element || !element.textContent) return;

const matches = [];
const { textContent } = element;
terms.forEach((term) => {
let start = 0;
let offset = textContent.toLowerCase().indexOf(term.toLowerCase(), start);
while (offset >= 0) {
matches.push({ offset, term: textContent.substring(offset, offset + term.length) });
start = offset + term.length;
offset = textContent.toLowerCase().indexOf(term.toLowerCase(), start);
}
});

if (!matches.length) {
return;
}

matches.sort((a, b) => a.offset - b.offset);
let currentIndex = 0;
const fragment = matches.reduce((acc, { offset, term }) => {
if (offset < currentIndex) return acc;
const textBefore = textContent.substring(currentIndex, offset);
if (textBefore) {
acc.appendChild(document.createTextNode(textBefore));
}
const markedTerm = document.createElement('mark');
markedTerm.textContent = term;
acc.appendChild(markedTerm);
currentIndex = offset + term.length;
return acc;
}, document.createDocumentFragment());
const textAfter = textContent.substring(currentIndex);
if (textAfter) {
fragment.appendChild(document.createTextNode(textAfter));
}
element.innerHTML = '';
element.appendChild(fragment);
});
}

export async function fetchData(source) {
const response = await fetch(source);
if (!response.ok) {
// eslint-disable-next-line no-console
console.error('error loading API response', response);
return null;
}

const json = await response.json();
if (!json) {
// eslint-disable-next-line no-console
console.error('empty API response', source);
return null;
}

return json.data;
}

function renderResult(result, searchTerms, titleTag) {
const li = document.createElement('li');
const a = document.createElement('a');
a.href = result.path;
if (result.image) {
const wrapper = document.createElement('div');
wrapper.className = 'search-result-image';
const pic = createOptimizedPicture(result.image, '', false, [{ width: '375' }]);
wrapper.append(pic);
a.append(wrapper);
}
if (result.title) {
const title = document.createElement(titleTag);
title.className = 'search-result-title';
const link = document.createElement('a');
link.href = result.path;
link.textContent = result.title;
highlightTextElements(searchTerms, [link]);
title.append(link);
a.append(title);
}
if (result.description) {
const description = document.createElement('p');
description.textContent = result.description;
highlightTextElements(searchTerms, [description]);
a.append(description);
}
li.append(a);
return li;
}

function clearSearchResults(block) {
const searchResults = block.querySelector('.search-results');
searchResults.innerHTML = '';
}

function clearSearch(block) {
clearSearchResults(block);
if (window.history.replaceState) {
const url = new URL(window.location.href);
url.search = '';
searchParams.delete('q');
window.history.replaceState({}, '', url.toString());
}
}

async function renderResults(block, config, filteredData, searchTerms) {
clearSearchResults(block);
const searchResults = block.querySelector('.search-results');
const headingTag = searchResults.dataset.h;

if (filteredData.length) {
searchResults.classList.remove('no-results');
filteredData.forEach((result) => {
const li = renderResult(result, searchTerms, headingTag);
searchResults.append(li);
});
} else {
const noResultsMessage = document.createElement('li');
searchResults.classList.add('no-results');
noResultsMessage.textContent = config.placeholders.searchNoResults || 'No results found.';
searchResults.append(noResultsMessage);
}
}

function compareFound(hit1, hit2) {
return hit1.minIdx - hit2.minIdx;
}

function filterData(searchTerms, data) {
const foundInHeader = [];
const foundInMeta = [];

data.forEach((result) => {
let minIdx = -1;

searchTerms.forEach((term) => {
const idx = (result.header || result.title).toLowerCase().indexOf(term);
if (idx < 0) return;
if (minIdx < idx) minIdx = idx;
});

if (minIdx >= 0) {
foundInHeader.push({ minIdx, result });
return;
}

const metaContents = `${result.title} ${result.description} ${result.path.split('/').pop()}`.toLowerCase();
searchTerms.forEach((term) => {
const idx = metaContents.indexOf(term);
if (idx < 0) return;
if (minIdx < idx) minIdx = idx;
});

if (minIdx >= 0) {
foundInMeta.push({ minIdx, result });
}
});

return [
...foundInHeader.sort(compareFound),
...foundInMeta.sort(compareFound),
].map((item) => item.result);
}

async function handleSearch(e, block, config) {
const searchValue = e.target.value;
searchParams.set('q', searchValue);
if (window.history.replaceState) {
const url = new URL(window.location.href);
url.search = searchParams.toString();
window.history.replaceState({}, '', url.toString());
}

if (searchValue.length < 3) {
clearSearch(block);
return;
}
const searchTerms = searchValue.toLowerCase().split(/\s+/).filter((term) => !!term);

const data = await fetchData(config.source);
const filteredData = filterData(searchTerms, data);
await renderResults(block, config, filteredData, searchTerms);
}

function searchResultsContainer(block) {
const results = document.createElement('ul');
results.className = 'search-results';
results.dataset.h = findNextHeading(block);
return results;
}

function searchInput(block, config) {
const input = document.createElement('input');
input.setAttribute('type', 'search');
input.className = 'search-input';

const searchPlaceholder = config.placeholders.searchPlaceholder || 'Search...';
input.placeholder = searchPlaceholder;
input.setAttribute('aria-label', searchPlaceholder);

input.addEventListener('input', (e) => {
handleSearch(e, block, config);
});

input.addEventListener('keyup', (e) => { if (e.code === 'Escape') { clearSearch(block); } });

return input;
}

function searchIcon() {
const icon = document.createElement('span');
icon.classList.add('icon', 'icon-search');
return icon;
}

function searchBox(block, config) {
const box = document.createElement('div');
box.classList.add('search-box');
box.append(
searchIcon(),
searchInput(block, config),
);

return box;
}

export default async function decorate(block) {
const placeholders = await fetchPlaceholders();
const source = block.querySelector('a[href]') ? block.querySelector('a[href]').href : '/query-index.json';
block.innerHTML = '';
block.append(
searchBox(block, { source, placeholders }),
searchResultsContainer(block),
);

if (searchParams.get('q')) {
const input = block.querySelector('input');
input.value = searchParams.get('q');
input.dispatchEvent(new Event('input'));
}

decorateIcons(block);
}

This is the CSS file that generates a fraction of the block named slide-builder. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/slide-builder/slide-builder.css
.slide-builder {
width: 100%;
}

.slide-builder-item {
height: 600px;
object-fit: cover;
position: relative; /* For roundel positioning */
margin-bottom: 25px; /* Add margin to the bottom of each slide */
}

/* Roundel Styling */
.slide-builder-item::before {
content: attr(data-slidenum);
position: absolute;
top: 20px;
left: 20px;
width: 40px;
height: 40px;
background-color: rgba(0, 0, 0, 0.7);
color: white;
border-radius: 50%;
display: flex; /* We still need this for centering the roundel content */
align-items: center;
justify-content: center;
font-weight: bold;
}

.slide-builder-item div {
background: rgba(0, 0, 0, 0.5);
color: white;
padding: 1em;
margin: 1em;
position: absolute; /* Position the div at the bottom left */
bottom: 1em;
left: 1em;
}

.slide-builder-item h2 {
margin: 0;
font-weight: bold;
}

/* Specific styling for the supporting text paragraph */
.slide-builder-item p.supporting-text {
margin: 0;
}

/* Media query for screens smaller than 800px */
@media (max-width: 800px) {
.slide-builder-item p.supporting-text {
display: none; /* Hide supporting text on smaller screens */
}
}

.slide-panel {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}

.slide-panel-content {
background-color: white;
padding: 20px;
border-radius: 5px;
max-width: 80%;
max-height: 80%;
overflow-y: auto;
position: relative;
}

.slide-panel-close {
position: absolute;
top: 10px;
right: 10px;
font-size: 24px;
background: none;
border: none;
cursor: pointer;
color: white; /* Make the button visible against dark background */
z-index: 1001; /* Ensure it's above the content */
}
This is the JS file that generates a fraction of the block named slide-builder. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/slide-builder/slide-builder.js
export default async function decorate(block) {
const supportsWebP = window.createImageBitmap && window.createImageBitmap.toString().includes("native code");

async function fetchSlideHtml(path) {
try {
const response = await fetch(`${path}.plain.html`);
if (!response.ok) {
throw new Error(`Failed to fetch HTML for slide: ${path}`);
}
return await response.text();
} catch (error) {
console.error(error);
return null;
}
}

async function fetchSlides() {
const response = await fetch("/slides/query-index.json");
const json = await response.json();

const slides = [];
for (const slide of json.data) {
if (window.innerWidth > 799) {
slide.html = await fetchSlideHtml(slide.path);
} else {
slide.html = null;
}
slides.push(slide);
}
return slides;
}

function fetchSupportingText(html) {
if (!html) return null;

const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");

const h2 = doc.querySelector("h2");
let firstParagraph = h2 ? h2.nextElementSibling : doc.querySelector("p");

while (firstParagraph && firstParagraph.tagName.toLowerCase() !== "p") {
firstParagraph = firstParagraph.nextElementSibling;
}

return firstParagraph?.textContent.trim() || null;
}

function setSlideBackground(slideItem, imageUrl) {
const finalImageUrl = supportsWebP
? `${imageUrl}?width=2000&format=webply&optimize=medium`
: imageUrl;

const img = new Image();
img.src = finalImageUrl;

img.onload = () => {
slideItem.style.backgroundImage = `url(${finalImageUrl})`;
slideItem.classList.add("loaded");
};

img.onerror = () => {
console.error(`Failed to load image: ${finalImageUrl}`);
};
}

async function createPanel(slideData) {
let html = slideData.html;
if (!html) {
html = await fetchSlideHtml(slideData.path);
if (!html) {
console.error("Failed to fetch HTML content for this slide");
return;
}
}

const panel = document.createElement('div');
panel.classList.add('slide-panel');
panel.innerHTML = `
<div class="slide-panel-content">
<div class="slide-panel-body"></div>
</div>
<button class="slide-panel-close" aria-label="Close panel">&times;</button>
`;
panel.querySelector('.slide-panel-body').innerHTML = html;

const closeButton = panel.querySelector('.slide-panel-close');
closeButton.addEventListener('click', () => {
panel.remove();
});

document.body.appendChild(panel);

// Ensure the close button is visible and above other content
setTimeout(() => {
closeButton.style.display = 'block';
closeButton.style.zIndex = '1001'; // Ensure this is higher than other elements
}, 0);
}

async function createSlideItem(slideData, index) {
const { image, title, description, path } = slideData;
const imageUrl = image.split("?")[0];

const slideItem = document.createElement("div");
slideItem.classList.add("slide-builder-item");
slideItem.setAttribute("data-bg", imageUrl);
slideItem.setAttribute("data-slidenum", index + 1);

slideItem.innerHTML = `
<div class="text-container">
<h2>${title}</h2>
<p><strong>${description}</strong></p>
</div>
`;

slideItem.addEventListener("click", () => createPanel(slideData));

// Fetch and append supporting text if available
if (!slideData.html && window.innerWidth <= 799) {
slideData.html = await fetchSlideHtml(path);
}

if (slideData.html) {
const supportingText = fetchSupportingText(slideData.html);
if (supportingText) {
const textContainer = slideItem.querySelector('.text-container');
textContainer.insertAdjacentHTML('beforeend', `
<p class="supporting-text">${supportingText}</p>
`);
}
}

return slideItem;
}

const container = document.querySelector(".slide-builder");
const slides = await fetchSlides();

const observer = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const slideItem = entry.target;
const imageUrl = slideItem.dataset.bg;
setSlideBackground(slideItem, imageUrl);
observer.unobserve(slideItem);
}
});
},
{ rootMargin: "100px" }
);

for (let i = 0; i < slides.length; i++) {
const slideItem = await createSlideItem(slides[i], i);
observer.observe(slideItem);
container.appendChild(slideItem);
}
}
This is the Markdown file that generates a fraction of the block named README. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/table/README.md
# table block

Table block is party of Adobe blocks, not included in the boilerplate; often used in projects, so added here for convenience

It has been modified to provide Aria symbols

This is the CSS file that generates a fraction of the block named table. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/table/table.css
.table {
width: 100%;
overflow-x: auto;
}

.table table {
width: 100%;
max-width: 100%;
border-collapse: collapse;
font-size: var(--body-font-size-xs);
}

@media (width >= 600px) {
.table table {
font-size: var(--body-font-size-s);
}
}

@media (width >= 900px) {
.table table {
font-size: var(--body-font-size-m);
}
}

.table table thead tr {
border-top: 2px solid;
border-bottom: 2px solid;
}

.table table tbody tr {
border-bottom: 1px solid;
}

.table table th {
font-weight: 700;
}

.table table th,
.table table td {
padding: 8px 16px;
text-align: left;
}

/* no header variant */
.table.no-header table tbody tr {
border-top: 1px solid;
}

/* striped variant */
.table.striped tbody tr:nth-child(odd) {
background-color: var(--overlay-background-color);
}

/* bordered variant */
.table.bordered table th,
.table.bordered table td {
border: 1px solid;
}

This is the JS file that generates a fraction of the block named table. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/table/table.js
/*
* Table Block
* Recreate a table
* https://www.hlx.live/developer/block-collection/table
* Modified to add aria
*/

function buildCell(rowIndex) {
const cell = rowIndex ? document.createElement('td') : document.createElement('th');
if (!rowIndex) cell.setAttribute('scope', 'col');
return cell;
}

export default async function decorate(block) {
const table = document.createElement('table');
const thead = document.createElement('thead');
const tbody = document.createElement('tbody');
table.append(thead, tbody);

const header = !block.classList.contains('no-header');
if (header) {
table.append(thead);
}
table.append(tbody);

[...block.children].forEach((child, i) => {
const row = document.createElement('tr');
if (header && i === 0) thead.append(row);
else thead.append(row);
[...child.children].forEach((col) => {
const cell = buildCell(header ? i : i + 1);
cell.innerHTML = col.innerHTML;
row.append(cell);
});
});

block.innerHTML = '';
block.append(table);

// Set the role of the table
table.setAttribute('role', 'table');

// Enhance each header cell
const headers = table.querySelectorAll('th');
headers.forEach((header, index) => {
header.setAttribute('scope', 'col');
header.setAttribute('role', 'columnheader');
header.id = `header-${index}`;
});

// Enhance each data cell
const cells = table.querySelectorAll('td');
cells.forEach((cell, index) => {
const columnIndex = index % table.rows[0].cells.length;
const headerId = `header-${columnIndex}`;
cell.setAttribute('aria-describedby', headerId);
});
}

This is the Markdown file that generates a fraction of the block named README. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/text/README.md
# text javascript

This code imports a function called `renderExpressions` from a JavaScript module located at "/plusplus/plugins/expressions/src/expressions.js". It then defines and exports a default function named `decorate` which takes a parameter called `block`.

Inside the `decorate` function, it selects an element with the class name 'text-wrapper' using `document.querySelector`. It then passes this selected element to the `renderExpressions` function, which presumably processes and renders expressions within that element.

In summary, this code snippet is responsible for decorating a specific block of content by rendering expressions within an element that has the class 'text-wrapper', utilizing the imported `renderExpressions` function from an external module.

This will just display text, but if any expressions are included it wil execute them

see <https://github.com/vtsaplin/franklin-expressions/>

Only one expression has been defined in clientExpressions.js {{expand,'$NAMESPACE:VARIABLE$}}

This one command expands the variable as text.

Create your own expressions in clientExpressions.js

This is the CSS file that generates a fraction of the block named text. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/text/text.css
/* stylelint-disable-next-line no-empty-source */
This is the JS file that generates a fraction of the block named text. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/text/text.js
/* eslint-disable import/no-absolute-path */
/* eslint-disable no-unused-vars */
/* eslint-disable import/no-unresolved */
import { renderExpressions } from '/plusplus/plugins/expressions/src/expressions.js';

export default function decorate(block) {
renderExpressions(document.querySelector('.text-wrapper'));
}

This is the Markdown file that generates a fraction of the block named README. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/video/README.md
# This block

This block is party of Adobe blocks, not included in the boilerplate; often used in projects, so added here for convenience

This is the CSS file that generates a fraction of the block named video. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/video/video.css
.video {
width: unset;
text-align: center;
max-width: 800px;
margin: 32px auto;
}

.video.lazy-loading {
/* reserve an approximate space to avoid extensive layout shifts */
aspect-ratio: 16 / 9;
}

.video > div {
display: flex;
justify-content: center;
}

.video video {
max-width: 100%;
}

.video video[data-loading] {
/* reserve an approximate space to avoid extensive layout shifts */
width: 100%;
aspect-ratio: 16 / 9;
}

.video .video-placeholder {
width: 100%;
aspect-ratio: 16 / 9;
position: relative;
}

.video .video-placeholder > * {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
inset: 0;
}

.video .video-placeholder picture img {
width: 100%;
height: 100%;
object-fit: cover;
}

.video .video-placeholder-play button {
box-sizing: border-box;
position: relative;
display: block;
transform: scale(3);
width: 22px;
height: 22px;
border: 2px solid;
border-radius: 20px;
padding: 0;
}

.video .video-placeholder-play button::before {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
width: 0;
height: 10px;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-left: 6px solid;
top: 4px;
left: 7px;
}

This is the JS file that generates a fraction of the block named video. path: ~/Documents/GitHub/allaboutV2FranklinAI/blocks/video/video.js
/*
* Video Block
* Show a video referenced by a link
* https://www.hlx.live/developer/block-collection/video
*/

function embedYoutube(url, replacePlaceholder, autoplay) {
const usp = new URLSearchParams(url.search);
let suffix = '';
if (replacePlaceholder || autoplay) {
const suffixParams = {
autoplay: '1',
mute: autoplay ? '1' : '0',
controls: autoplay ? '0' : '1',
disablekb: autoplay ? '1' : '0',
loop: autoplay ? '1' : '0',
playsinline: autoplay ? '1' : '0',
};
suffix = `&${Object.entries(suffixParams).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&')}`;
}
let vid = usp.get('v') ? encodeURIComponent(usp.get('v')) : '';
const embed = url.pathname;
if (url.origin.includes('youtu.be')) {
[, vid] = url.pathname.split('/');
}

const temp = document.createElement('div');
temp.innerHTML = `<div style="left: 0; width: 100%; height: 0; position: relative; padding-bottom: 56.25%;">
<iframe src="https://www.youtube.com${vid ? `/embed/${vid}?rel=0&v=${vid}${suffix}` : embed}" style="border: 0; top: 0; left: 0; width: 100%; height: 100%; position: absolute;"
allow="autoplay; fullscreen; picture-in-picture; encrypted-media; accelerometer; gyroscope; picture-in-picture" allowfullscreen="" scrolling="no" title="Content from Youtube" loading="lazy"></iframe>
</div>`;
return temp.children.item(0);
}

function embedVimeo(url, replacePlaceholder, autoplay) {
const [, video] = url.pathname.split('/');
let suffix = '';
if (replacePlaceholder || autoplay) {
const suffixParams = {
autoplay: '1',
background: autoplay ? '1' : '0',
};
suffix = `?${Object.entries(suffixParams).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&')}`;
}
const temp = document.createElement('div');
temp.innerHTML = `<div style="left: 0; width: 100%; height: 0; position: relative; padding-bottom: 56.25%;">
<iframe src="https://player.vimeo.com/video/${video}${suffix}"
style="border: 0; top: 0; left: 0; width: 100%; height: 100%; position: absolute;"
frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen
title="Content from Vimeo" loading="lazy"></iframe>
</div>`;
return temp.children.item(0);
}

function getVideoElement(source, replacePlaceholder, autoplay) {
const video = document.createElement('video');
video.setAttribute('controls', '');
video.dataset.loading = 'true';
video.addEventListener('loadedmetadata', () => delete video.dataset.loading);
if (autoplay || replacePlaceholder) {
video.setAttribute('autoplay', '');
if (autoplay) {
video.setAttribute('loop', '');
video.setAttribute('playsinline', '');
video.removeAttribute('controls');
video.addEventListener('canplay', () => {
video.muted = true;
video.play();
});
}
}

const sourceEl = document.createElement('source');
sourceEl.setAttribute('src', source);
sourceEl.setAttribute('type', `video/${source.split('.').pop()}`);
video.append(sourceEl);

return video;
}

const loadVideoEmbed = (block, link, replacePlaceholder, autoplay) => {
if (block.dataset.embedIsLoaded) {
return;
}
const url = new URL(link);

const isYoutube = link.includes('youtube') || link.includes('youtu.be');
const isVimeo = link.includes('vimeo');
const isMp4 = link.includes('.mp4');

let embedEl;
if (isYoutube) {
embedEl = embedYoutube(url, replacePlaceholder, autoplay);
} else if (isVimeo) {
embedEl = embedVimeo(url, replacePlaceholder, autoplay);
} else if (isMp4) {
embedEl = getVideoElement(link, replacePlaceholder, autoplay);
}
block.replaceChildren(embedEl);

block.dataset.embedIsLoaded = true;
};

export default async function decorate(block) {
const placeholder = block.querySelector('picture');
const link = block.querySelector('a').href;
block.textContent = '';

if (placeholder) {
const wrapper = document.createElement('div');
wrapper.className = 'video-placeholder';
wrapper.innerHTML = '<div class="video-placeholder-play"><button type="button" title="Play"></button></div>';
wrapper.prepend(placeholder);
wrapper.addEventListener('click', () => {
loadVideoEmbed(block, link, true, false);
});
block.append(wrapper);
} else {
block.classList.add('lazy-loading');
const observer = new IntersectionObserver((entries) => {
if (entries.some((e) => e.isIntersecting)) {
observer.disconnect();
loadVideoEmbed(block, link, false, block.classList.contains('autoplay'));
block.classList.remove('lazy-loading');
}
});
observer.observe(block);
}
}

This is a CSS file that contains the overarching styles. path: ~/Documents/GitHub/allaboutV2FranklinAI/styles/fonts.css
@font-face {
font-family: roboto;
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('../fonts/roboto-bold.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
font-family: roboto;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('../fonts/roboto-regular.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

This is a CSS file that contains the overarching styles. path: ~/Documents/GitHub/allaboutV2FranklinAI/styles/lazy-styles.css
/* add global styles that can be loaded post LCP here */
This is a CSS file that contains the overarching styles. path: ~/Documents/GitHub/allaboutV2FranklinAI/styles/styles.css
/*
* Copyright 2020 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

:root {
/* colors */
--link-color: #035fe6;
--link-hover-color: #136ff6;
--background-color: white;
--light-color: #eee;
--dark-color: #ccc;
--text-color: black;

/* fonts */
--body-font-family: roboto, roboto-fallback;
--heading-font-family: var(--body-font-family);
--fixed-font-family: 'Roboto Mono', menlo, consolas, 'Liberation Mono', monospace;

/* body sizes */
--body-font-size-m: 22px;
--body-font-size-s: 18px;
--body-font-size-xs: 16px;

/* heading sizes */
--heading-font-size-xxl: 48px;
--heading-font-size-xl: 40px;
--heading-font-size-l: 32px;
--heading-font-size-m: 24px;
--heading-font-size-s: 20px;
--heading-font-size-xs: 18px;

/* nav height */
--nav-height: 64px;
}

@font-face {
font-family: roboto-fallback;
size-adjust: 100.06%;
ascent-override: 95%;
src: local('Arial');
}

@media (width >= 900px) {
:root {
--heading-font-size-xxl: 60px;
--heading-font-size-xl: 48px;
--heading-font-size-l: 36px;
--heading-font-size-m: 30px;
--heading-font-size-s: 24px;
--heading-font-size-xs: 22px;
}
}

body {
font-size: var(--body-font-size-m);
margin: 0;
font-family: var(--body-font-family);
line-height: 1.6;
color: var(--text-color);
background-color: var(--background-color);
display: none;
}

body.appear {
display: block;
}

header {
height: var(--nav-height);
}

h1, h2, h3,
h4, h5, h6 {
font-family: var(--heading-font-family);
font-weight: 600;
line-height: 1.25;
margin-top: 1em;
margin-bottom: .5em;
scroll-margin: calc(var(--nav-height) + 1em);
}

h1 { font-size: var(--heading-font-size-xxl) }
h2 { font-size: var(--heading-font-size-xl) }
h3 { font-size: var(--heading-font-size-l) }
h4 { font-size: var(--heading-font-size-m) }
h5 { font-size: var(--heading-font-size-s) }
h6 { font-size: var(--heading-font-size-xs) }

p, dl, ol, ul, pre, blockquote {
margin-top: 1em;
margin-bottom: 1em;
}

code, pre {
font-family: var(--fixed-font-family);
font-size: var(--body-font-size-s);
}

code {
padding: .125em;
}

pre {
overflow: scroll;
}

main pre {
background-color: var(--light-color);
padding: 1em;
border-radius: .25em;
overflow-x: auto;
white-space: pre;
}

/* links */
a:any-link {
color: var(--link-color);
text-decoration: none;
}

a:hover {
text-decoration: underline;
color: var(--link-hover-color);
}

/* buttons */
a.button:any-link, button {
font-family: var(--body-font-family);
display: inline-block;
box-sizing: border-box;
text-decoration: none;
border: 2px solid transparent;
padding: 5px 30px;
text-align: center;
font-style: normal;
font-weight: 600;
cursor: pointer;
color: var(--background-color);
background-color: var(--link-color);
margin: 16px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
border-radius: 30px;
}

a.button:hover, a.button:focus, button:hover, button:focus {
background-color: var(--link-hover-color);
cursor: pointer;
}

button:disabled, buttonhover {
background-color: var(--light-color);
cursor: unset;
}

a.button.secondary, button.secondary {
background-color: unset;
border: 2px solid currentcolor;
color: var(--text-color)
}

main img {
max-width: 100%;
width: auto;
height: auto;
}

.icon {
display: inline-block;
height: 24px;
width: 24px;
}

.icon img {
height: 100%;
width: 100%;
}

/* sections */
main .section {
padding: 64px 16px;
}

@media (width >= 600px) {
main .section {
padding: 64px 32px;
}
}

@media (width >= 900px) {
.section > div {
max-width: 1200px;
margin: auto;
}
}

/* section metadata */
main .section.light,
main .section.highlight {
background-color: var(--light-color);
}

main .section.bg-dark {background-color: var(--dark-color);
}

# AI Assistant Guide for Adobe Edge Delivery Services (EDS) Development

As an expert AI assistant specializing in Adobe Edge Delivery Services (EDS), also known as Helix or Franklin, your primary responsibility is to assist developers in creating, explaining, and optimizing code for EDS projects. You must follow EDS-specific best practices and conventions.

## Core EDS Concepts

1. **Document-based authoring:** Content is authored using Google Docs or Microsoft Word.
2. **Serverless architecture:** Focus on performance and scalability.
3. **GitHub integration:** Code is stored and synced via GitHub repositories.
4. **Block-based development:** Blocks are key components for functionality and styling.
5. **Modern web technologies:** Use vanilla JavaScript and CSS3. Avoid external libraries unless necessary.
6. **Responsive design:** Apply a mobile-first approach to all components.
7. **Accessibility and SEO:** These should always be prioritized.
8. **E-L-D loading pattern:** Use Eager, Lazy, and Delayed loading for performance.

## Project Structure

```shell
/
├── blocks/
│ ├── header/
│ │ ├── header.css
│ │ └── header.js
│ └── footer/
│ ├── footer.css
│ └── footer.js
├── scripts/
│ ├── aem.js
│ └── scripts.js
├── styles/
│ └── styles.css
├── tools/
│ └── sidekick/
│ └── config.json
├── 404.html
├── 500.html
├── head.html
├── helix-query.yaml
├── helix-sitemap.yaml
└── fstab.yaml
```

## Block Development Guidelines

### File Structure:

Each block should have its own directory:

```
/blocks/blockname/
├── blockname.js
├── blockname.css
└── README.md
```

### CSS File (`blockname.css`)

- Focus on styles specific to the block.
- Implement a responsive design using CSS variables for theming.

```css
.blockname {
/* Base styles */
}

.blockname__element {
/* Element styles */
}

@media (max-width: 768px) {
/* Responsive styles */
}
```

### JavaScript File (`blockname.js`)

- Export a `decorate` function as default.
- Structure the function to handle DOM manipulation, event listeners, data fetching, and dynamic styling.
- Use async/await for asynchronous tasks.

```javascript
export default async function decorate(block) {
const container = document.createElement('div');
block.appendChild(container);

container.addEventListener('click', () => {
// Interaction logic
});

try {
const response = await fetch('/path/to/data.json');
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
updateBlockWithData(container, data);
} catch (error) {
displayErrorMessage(container);
}

block.classList.add('blockname--initialized');
}

function updateBlockWithData(container, data) {
// Update the block with fetched data
}

function displayErrorMessage(container) {
// Display an error message in the block
}
```

### README.md

Include the following:
1. Block name and description
2. Usage instructions
3. Configuration options
4. Variations with examples
5. Dependencies
6. Accessibility notes
7. Performance considerations
8. Author/version details

## Best Practices

1. **Modularity:** Keep blocks self-contained and reusable.
2. **Semantic HTML:** Use appropriate elements to ensure structured content.
3. **Accessibility:** Implement ARIA attributes and keyboard navigation.
4. **Performance:** Prioritize performance using lazy loading and efficient DOM manipulation.
5. **Naming Conventions:** Use consistent and descriptive class/ID names.
6. **Responsive Design:** Ensure adaptability to various screen sizes.
7. **Error Handling:** Implement graceful error handling.
8. **Code Style:** Follow the Airbnb JavaScript Style Guide for clean, maintainable code.
9. **Async Operations:** Use async/await consistently for asynchronous tasks.

## Advanced Features

- Lazy loading of images/content
- Infinite scrolling for content-heavy blocks
- API integration
- Complex animations and transitions
- State management for interactive blocks

## Supporting Developers

1. Provide functional, complete code snippets.
2. Explain every significant function and block of code.
3. Consider performance implications and suggest optimizations.
4. Balance performance with a good content authoring experience.
5. Adapt suggestions based on project-specific needs.

## Spreadsheet Usage

- Use spreadsheets for dynamic content or configuration. The system converts them to JSON.
- Access content via `/path/to/spreadsheet.json`.

## Advanced Development

- Use `query-index.json` for dynamic site-wide content.
- Configure headers, redirects, and authentication using the `.helix` folder.

## Block Development Details

1. **CSS:** Use CSS3 and variables. Create variations using additional classes.
2. **JavaScript:** Use modern browser APIs like fetch and Intersection Observer.
3. **HTML Structure:** Wrap blocks in `<div class="blockname-wrapper">`. Inner content resides in `<div class="blockname block">`.

## Key EDS Features

1. **Performance:** Leverage EDS features for optimal page speed. Minimize external dependencies and lazy-load heavy content.
2. **Modularity:** Keep blocks self-contained and reusable. Use variations for different scenarios. Separate concerns (HTML, CSS, JavaScript).
3. **Author Experience:** Make block usage intuitive. Provide documentation. Offer sensible defaults with customizability.
4. **SEO & Accessibility:** Ensure proper heading structures, use semantic HTML, and add alt text to images.
5. **Responsive Design:** Design mobile-first, then enhance for larger screens using CSS Grid and Flexbox.

## Configuration and Advanced Features

1. **`.helix` folder:** Stores configuration files for headers, redirects, and authentication. It's not publicly accessible.
2. **Spreadsheets and JSON:** Use `query-index` for site-wide content and dynamic configurations. GitHub acts as a fallback for spreadsheets.
3. **Authentication:** Managed in the `.helix/config` spreadsheet.
4. **Redirects and HTTP Headers:** Managed via spreadsheets or GitHub fallback.

Always strive to create efficient, well-documented code that aligns with EDS/Helix/Franklin development standards. Adapt block structure based on project requirements, consistently using async/await, and provide clear, functional examples to developers. Follow the Airbnb JavaScript Style Guide unless specified otherwise.if writing code that uses console output, remember to precede it with // eslint-disable-next-line no-console